Anonymous page caching is unsafe for visitors carrying session-state cookies: a WooCommerce customer with items in cart (woocommerce_items_in_cart, wp_woocommerce_session_*), password-protected post visitors (wp-postpass), or comment authors must always receive dynamic pages, not just logged-in users.
Problem: WP Recache only bypasses cache for wordpress_logged_in. A shopper with items in cart gets cached pages everywhere (stale mini-cart, wrong tax prices), breaking the intent of story 5 in #1.
Spec:
New rejected_cookies option (defaults: WooCommerce session/cart cookies, wp-postpass, comment author cookies), editable in settings
wp_recache_should_exclude() returns true when a rejected cookie is present
The standalone drop-in (wp-content/wp-recache-cache.php) applies the same check — cookie names are readable without WordPress
Updating the option purges the cache
Anonymous page caching is unsafe for visitors carrying session-state cookies: a WooCommerce customer with items in cart (woocommerce_items_in_cart, wp_woocommerce_session_*), password-protected post visitors (wp-postpass), or comment authors must always receive dynamic pages, not just logged-in users.
**Problem:** WP Recache only bypasses cache for wordpress_logged_in. A shopper with items in cart gets cached pages everywhere (stale mini-cart, wrong tax prices), breaking the intent of story 5 in #1.
**Spec:**
- New rejected_cookies option (defaults: WooCommerce session/cart cookies, wp-postpass, comment author cookies), editable in settings
- wp_recache_should_exclude() returns true when a rejected cookie is present
- The standalone drop-in (wp-content/wp-recache-cache.php) applies the same check — cookie names are readable without WordPress
- Updating the option purges the cache
WP Recache only bypasses page caching for visitors carrying the wordpress_logged_in cookie. Anonymous visitors with session-state cookies — WooCommerce customers with items in cart (woocommerce_items_in_cart, wp_woocommerce_session_*), password-protected post visitors (wp-postpass), or comment authors (comment_author_*, wordpress_* comment cookies) — receive stale cached pages. This breaks dynamic elements like mini-carts showing wrong items, incorrect tax/pricing, and password-gate state.
Solution
Add a rejected_cookies option containing a list of cookie names (one per line) that trigger cache bypass. Defaults cover WooCommerce session/cart cookies, wp-postpass, and comment-author cookies. Both the WP-level exclusion check and the standalone drop-in apply the same list. Editing the option in settings purges all cache.
User Stories
As a WooCommerce store owner, I want visitors with items in cart to bypass the page cache, so they always see accurate mini-cart contents and tax prices.
As a site administrator, I want to see a "Rejected Cookies" textarea in WP Recache settings, so I can view and edit which cookies bypass the cache.
As a site administrator, I want the rejected cookies list to ship with sensible WooCommerce defaults pre-filled, so the feature works out of the box without manual configuration.
As a site administrator, I want to add custom cookie names to the rejected list, so I can handle plugins beyond WooCommerce that use session cookies.
As a site administrator, I want to remove cookie names from the rejected list, so I can fine-tune cache bypass for my specific setup.
As a site administrator, I want the cache to be purged automatically when I save changes to the rejected cookies list, so stale cached pages are not served with the old rules.
As a WooCommerce customer, I want the mini-cart to show correct items and quantities on every page, so I can trust what I'm about to purchase.
As a password-protected post visitor, I want to receive a dynamic page when I carry a wp-postpass cookie, so the content gate works correctly.
As a comment author, I want to see my pending comment state reflected without requiring a page refresh, so I know my comment was submitted.
As a site administrator, I want the drop-in (served before WordPress loads) to also check rejected cookies, so the cache bypass works even on fast-path requests.
As a site administrator, I want the drop-in to read the same cookie list as the WP-level check, so there are no inconsistencies between the two cache-serving paths.
As a developer, I want the rejected cookies option to use the same wp_recache_ prefix and option pattern as existing options, so the codebase stays consistent.
As a site administrator, I want the rejected cookies setting to appear on the Cache tab alongside the existing "Exclude URLs" setting, so related exclusion controls are in one place.
As a site administrator, I want each rejected cookie name on its own line in the textarea, so the list is easy to read and edit.
As a site administrator, I want whitespace around cookie names to be trimmed automatically, so typos like accidental spaces don't cause missed cache bypasses.
As a site administrator, I want empty lines in the rejected cookies textarea to be ignored, so the list is forgiving of formatting inconsistencies.
As a site administrator, I want the default rejected cookies to include woocommerce_items_in_cart, wp_woocommerce_session_*, wp-postpass, and comment_author_*, so the most common session-state cookies are covered.
As a site administrator, I want the * wildcard suffix on cookie names (like wp_woocommerce_session_* and comment_author_*) to match any cookie name starting with that prefix, so session cookies with dynamic suffixes are handled.
As a developer, I want the drop-in to receive the rejected cookies list at activation time by writing it into the generated PHP file, so the drop-in can check cookies without loading WordPress or reading the database.
As a developer, I want the drop-in to be regenerated when the rejected cookies option is updated, so the drop-in always reflects the current list.
As a site administrator, I want the settings page to show a description explaining what rejected cookies do, so I understand the impact of my changes.
As a site administrator, I want the rejected cookies feature to work independently of the "Enable Cache" toggle — the option is always stored, so re-enabling cache after disabling it retains my configuration.
As a developer, I want wp_recache_should_exclude() to check rejected cookies after the logged-in check and before the URL exclusion check, so the check order is logical and consistent.
As a site administrator, I want the rejected cookies list to support both exact cookie names and prefix patterns with *, so I can handle WordPress cookies that include dynamic user-specific suffixes.
As a WooCommerce store owner, I want the default list to cover all standard WooCommerce session cookies, so I don't need to research cookie names manually.
Implementation Decisions
New option:wp_recache_rejected_cookies stored as an array of strings. Default value: ['woocommerce_items_in_cart', 'wp_woocommerce_session_*', 'wp-postpass', 'comment_author_*', 'wordpress_*']. The wordpress_* wildcard covers wordpress_logged_in_* as well, making the existing explicit logged-in check a redundant but safe fast-path.
Option registration: Register in Admin::register_settings() following the same pattern as wp_recache_exclude_urls: type array, sanitize callback splits on newlines, trims, filters empties, and applies sanitize_text_field.
Settings UI: Add a textarea row on the Cache tab, immediately below the "Exclude URLs" textarea. Same layout pattern: <textarea> with one cookie name per line, description paragraph.
WP-level check: In wp_recache_should_exclude(), after the logged-in check and before the URL exclusion check, iterate over the rejected cookies option. For each entry: if it ends with *, check strpos() against all $_COOKIE keys; otherwise check isset(). Return true on first match.
Drop-in generation:Activator::create_cache_server() bakes the rejected cookies list into the generated PHP file as a PHP array literal. The drop-in iterates this array using the same exact/prefix logic. No WordPress functions are called.
Drop-in regeneration on option change: Add an update_option_wp_recache_rejected_cookies hook in Admin::init() that calls Activator::create_cache_server() followed by wp_recache_delete_all_cache(). Same pattern as the existing update_option_wp_recache_exclude_urls hook.
Cache purge on save: Both update_option hooks purge all cache, ensuring stale pages are not served under new rules.
Wildcard convention: An asterisk at the end of a cookie name means "match any cookie whose name starts with this prefix." No other wildcard positions are supported. This is a simple strpos prefix check, not glob or regex.
Filter hook: The existing apply_filters('wp_recache_exclude', false) at the end of wp_recache_should_exclude() remains. The rejected cookies check runs before it, so filter-based exclusions still work as a fallback.
Testing Decisions
Unit tests in test-helpers.php: Add tests for wp_recache_should_exclude() returning true when a rejected cookie is present (exact match and prefix match), and false when no rejected cookies are present. Follow the existing pattern of setting $_COOKIE superglobal and asserting against the function return.
Unit tests in test-admin.php: Add tests for the sanitize callback: trimming, empty-line filtering, and array-vs-string input handling.
Manual tests (TESTING.md): Add a section covering: (a) visit with woocommerce_items_in_cart cookie set → MISS, no cache served; (b) visit with wp-postpass cookie → MISS; (c) edit rejected cookies in settings → cache purged; (d) drop-in serves dynamic page when rejected cookie is present.
Good test behavior: Tests assert external behavior (does the function return true/false? is the cache purged?) not implementation details (which line of code ran).
Out of Scope
No changes to cache TTL logic.
No new UI for per-URL cookie overrides.
No automatic detection of installed plugins to pre-fill cookie names.
No changes to the preload crawler.
No changes to mobile cache detection.
No changes to the DONOTCACHEPAGE constant handling.
No database schema changes beyond the new option row.
Further Notes
Issue #12 ("Bake settings into drop-in config file") is a related companion. If #12 lands first, the drop-in could read from a baked config file instead of regenerated PHP. This spec assumes the current approach (regenerated PHP) and is compatible with either direction.
The wordpress_* wildcard in defaults subsumes the existing explicit wordpress_logged_in check in the drop-in. The explicit check can remain as a fast-path without harm, or be removed once the wildcard is proven — a decision for implementation time.
The design doc (docs/plans/2026-09-13-wprecache-design.md) notes the drop-in cannot read WP options. This spec respects that constraint by baking the option value into the generated file.
## Spec: Rejected-cookie cache exclusions (WooCommerce cart)
### Problem Statement
WP Recache only bypasses page caching for visitors carrying the `wordpress_logged_in` cookie. Anonymous visitors with session-state cookies — WooCommerce customers with items in cart (`woocommerce_items_in_cart`, `wp_woocommerce_session_*`), password-protected post visitors (`wp-postpass`), or comment authors (`comment_author_*`, `wordpress_*` comment cookies) — receive stale cached pages. This breaks dynamic elements like mini-carts showing wrong items, incorrect tax/pricing, and password-gate state.
### Solution
Add a `rejected_cookies` option containing a list of cookie names (one per line) that trigger cache bypass. Defaults cover WooCommerce session/cart cookies, `wp-postpass`, and comment-author cookies. Both the WP-level exclusion check and the standalone drop-in apply the same list. Editing the option in settings purges all cache.
### User Stories
1. As a WooCommerce store owner, I want visitors with items in cart to bypass the page cache, so they always see accurate mini-cart contents and tax prices.
2. As a site administrator, I want to see a "Rejected Cookies" textarea in WP Recache settings, so I can view and edit which cookies bypass the cache.
3. As a site administrator, I want the rejected cookies list to ship with sensible WooCommerce defaults pre-filled, so the feature works out of the box without manual configuration.
4. As a site administrator, I want to add custom cookie names to the rejected list, so I can handle plugins beyond WooCommerce that use session cookies.
5. As a site administrator, I want to remove cookie names from the rejected list, so I can fine-tune cache bypass for my specific setup.
6. As a site administrator, I want the cache to be purged automatically when I save changes to the rejected cookies list, so stale cached pages are not served with the old rules.
7. As a WooCommerce customer, I want the mini-cart to show correct items and quantities on every page, so I can trust what I'm about to purchase.
8. As a password-protected post visitor, I want to receive a dynamic page when I carry a `wp-postpass` cookie, so the content gate works correctly.
9. As a comment author, I want to see my pending comment state reflected without requiring a page refresh, so I know my comment was submitted.
10. As a site administrator, I want the drop-in (served before WordPress loads) to also check rejected cookies, so the cache bypass works even on fast-path requests.
11. As a site administrator, I want the drop-in to read the same cookie list as the WP-level check, so there are no inconsistencies between the two cache-serving paths.
12. As a developer, I want the rejected cookies option to use the same `wp_recache_` prefix and option pattern as existing options, so the codebase stays consistent.
13. As a site administrator, I want the rejected cookies setting to appear on the Cache tab alongside the existing "Exclude URLs" setting, so related exclusion controls are in one place.
14. As a site administrator, I want each rejected cookie name on its own line in the textarea, so the list is easy to read and edit.
15. As a site administrator, I want whitespace around cookie names to be trimmed automatically, so typos like accidental spaces don't cause missed cache bypasses.
16. As a site administrator, I want empty lines in the rejected cookies textarea to be ignored, so the list is forgiving of formatting inconsistencies.
17. As a site administrator, I want the default rejected cookies to include `woocommerce_items_in_cart`, `wp_woocommerce_session_*`, `wp-postpass`, and `comment_author_*`, so the most common session-state cookies are covered.
18. As a site administrator, I want the `*` wildcard suffix on cookie names (like `wp_woocommerce_session_*` and `comment_author_*`) to match any cookie name starting with that prefix, so session cookies with dynamic suffixes are handled.
19. As a developer, I want the drop-in to receive the rejected cookies list at activation time by writing it into the generated PHP file, so the drop-in can check cookies without loading WordPress or reading the database.
20. As a developer, I want the drop-in to be regenerated when the rejected cookies option is updated, so the drop-in always reflects the current list.
21. As a site administrator, I want the settings page to show a description explaining what rejected cookies do, so I understand the impact of my changes.
22. As a site administrator, I want the rejected cookies feature to work independently of the "Enable Cache" toggle — the option is always stored, so re-enabling cache after disabling it retains my configuration.
23. As a developer, I want `wp_recache_should_exclude()` to check rejected cookies after the logged-in check and before the URL exclusion check, so the check order is logical and consistent.
24. As a site administrator, I want the rejected cookies list to support both exact cookie names and prefix patterns with `*`, so I can handle WordPress cookies that include dynamic user-specific suffixes.
25. As a WooCommerce store owner, I want the default list to cover all standard WooCommerce session cookies, so I don't need to research cookie names manually.
### Implementation Decisions
- **New option:** `wp_recache_rejected_cookies` stored as an array of strings. Default value: `['woocommerce_items_in_cart', 'wp_woocommerce_session_*', 'wp-postpass', 'comment_author_*', 'wordpress_*']`. The `wordpress_*` wildcard covers `wordpress_logged_in_*` as well, making the existing explicit logged-in check a redundant but safe fast-path.
- **Option registration:** Register in `Admin::register_settings()` following the same pattern as `wp_recache_exclude_urls`: type `array`, sanitize callback splits on newlines, trims, filters empties, and applies `sanitize_text_field`.
- **Settings UI:** Add a textarea row on the Cache tab, immediately below the "Exclude URLs" textarea. Same layout pattern: `<textarea>` with one cookie name per line, description paragraph.
- **WP-level check:** In `wp_recache_should_exclude()`, after the logged-in check and before the URL exclusion check, iterate over the rejected cookies option. For each entry: if it ends with `*`, check `strpos()` against all `$_COOKIE` keys; otherwise check `isset()`. Return `true` on first match.
- **Drop-in generation:** `Activator::create_cache_server()` bakes the rejected cookies list into the generated PHP file as a PHP array literal. The drop-in iterates this array using the same exact/prefix logic. No WordPress functions are called.
- **Drop-in regeneration on option change:** Add an `update_option_wp_recache_rejected_cookies` hook in `Admin::init()` that calls `Activator::create_cache_server()` followed by `wp_recache_delete_all_cache()`. Same pattern as the existing `update_option_wp_recache_exclude_urls` hook.
- **Cache purge on save:** Both `update_option` hooks purge all cache, ensuring stale pages are not served under new rules.
- **Wildcard convention:** An asterisk at the end of a cookie name means "match any cookie whose name starts with this prefix." No other wildcard positions are supported. This is a simple `strpos` prefix check, not glob or regex.
- **Filter hook:** The existing `apply_filters('wp_recache_exclude', false)` at the end of `wp_recache_should_exclude()` remains. The rejected cookies check runs before it, so filter-based exclusions still work as a fallback.
### Testing Decisions
- **Unit tests in `test-helpers.php`:** Add tests for `wp_recache_should_exclude()` returning `true` when a rejected cookie is present (exact match and prefix match), and `false` when no rejected cookies are present. Follow the existing pattern of setting `$_COOKIE` superglobal and asserting against the function return.
- **Unit tests in `test-admin.php`:** Add tests for the sanitize callback: trimming, empty-line filtering, and array-vs-string input handling.
- **Manual tests (TESTING.md):** Add a section covering: (a) visit with `woocommerce_items_in_cart` cookie set → MISS, no cache served; (b) visit with `wp-postpass` cookie → MISS; (c) edit rejected cookies in settings → cache purged; (d) drop-in serves dynamic page when rejected cookie is present.
- **Good test behavior:** Tests assert external behavior (does the function return true/false? is the cache purged?) not implementation details (which line of code ran).
### Out of Scope
- No changes to cache TTL logic.
- No new UI for per-URL cookie overrides.
- No automatic detection of installed plugins to pre-fill cookie names.
- No changes to the preload crawler.
- No changes to mobile cache detection.
- No changes to the `DONOTCACHEPAGE` constant handling.
- No database schema changes beyond the new option row.
### Further Notes
- Issue #12 ("Bake settings into drop-in config file") is a related companion. If #12 lands first, the drop-in could read from a baked config file instead of regenerated PHP. This spec assumes the current approach (regenerated PHP) and is compatible with either direction.
- The `wordpress_*` wildcard in defaults subsumes the existing explicit `wordpress_logged_in` check in the drop-in. The explicit check can remain as a fast-path without harm, or be removed once the wildcard is proven — a decision for implementation time.
- The design doc (`docs/plans/2026-09-13-wprecache-design.md`) notes the drop-in cannot read WP options. This spec respects that constraint by baking the option value into the generated file.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Anonymous page caching is unsafe for visitors carrying session-state cookies: a WooCommerce customer with items in cart (woocommerce_items_in_cart, wp_woocommerce_session_*), password-protected post visitors (wp-postpass), or comment authors must always receive dynamic pages, not just logged-in users.
Problem: WP Recache only bypasses cache for wordpress_logged_in. A shopper with items in cart gets cached pages everywhere (stale mini-cart, wrong tax prices), breaking the intent of story 5 in #1.
Spec:
Spec: Rejected-cookie cache exclusions (WooCommerce cart)
Problem Statement
WP Recache only bypasses page caching for visitors carrying the
wordpress_logged_incookie. Anonymous visitors with session-state cookies — WooCommerce customers with items in cart (woocommerce_items_in_cart,wp_woocommerce_session_*), password-protected post visitors (wp-postpass), or comment authors (comment_author_*,wordpress_*comment cookies) — receive stale cached pages. This breaks dynamic elements like mini-carts showing wrong items, incorrect tax/pricing, and password-gate state.Solution
Add a
rejected_cookiesoption containing a list of cookie names (one per line) that trigger cache bypass. Defaults cover WooCommerce session/cart cookies,wp-postpass, and comment-author cookies. Both the WP-level exclusion check and the standalone drop-in apply the same list. Editing the option in settings purges all cache.User Stories
wp-postpasscookie, so the content gate works correctly.wp_recache_prefix and option pattern as existing options, so the codebase stays consistent.woocommerce_items_in_cart,wp_woocommerce_session_*,wp-postpass, andcomment_author_*, so the most common session-state cookies are covered.*wildcard suffix on cookie names (likewp_woocommerce_session_*andcomment_author_*) to match any cookie name starting with that prefix, so session cookies with dynamic suffixes are handled.wp_recache_should_exclude()to check rejected cookies after the logged-in check and before the URL exclusion check, so the check order is logical and consistent.*, so I can handle WordPress cookies that include dynamic user-specific suffixes.Implementation Decisions
wp_recache_rejected_cookiesstored as an array of strings. Default value:['woocommerce_items_in_cart', 'wp_woocommerce_session_*', 'wp-postpass', 'comment_author_*', 'wordpress_*']. Thewordpress_*wildcard coverswordpress_logged_in_*as well, making the existing explicit logged-in check a redundant but safe fast-path.Admin::register_settings()following the same pattern aswp_recache_exclude_urls: typearray, sanitize callback splits on newlines, trims, filters empties, and appliessanitize_text_field.<textarea>with one cookie name per line, description paragraph.wp_recache_should_exclude(), after the logged-in check and before the URL exclusion check, iterate over the rejected cookies option. For each entry: if it ends with*, checkstrpos()against all$_COOKIEkeys; otherwise checkisset(). Returntrueon first match.Activator::create_cache_server()bakes the rejected cookies list into the generated PHP file as a PHP array literal. The drop-in iterates this array using the same exact/prefix logic. No WordPress functions are called.update_option_wp_recache_rejected_cookieshook inAdmin::init()that callsActivator::create_cache_server()followed bywp_recache_delete_all_cache(). Same pattern as the existingupdate_option_wp_recache_exclude_urlshook.update_optionhooks purge all cache, ensuring stale pages are not served under new rules.strposprefix check, not glob or regex.apply_filters('wp_recache_exclude', false)at the end ofwp_recache_should_exclude()remains. The rejected cookies check runs before it, so filter-based exclusions still work as a fallback.Testing Decisions
test-helpers.php: Add tests forwp_recache_should_exclude()returningtruewhen a rejected cookie is present (exact match and prefix match), andfalsewhen no rejected cookies are present. Follow the existing pattern of setting$_COOKIEsuperglobal and asserting against the function return.test-admin.php: Add tests for the sanitize callback: trimming, empty-line filtering, and array-vs-string input handling.woocommerce_items_in_cartcookie set → MISS, no cache served; (b) visit withwp-postpasscookie → MISS; (c) edit rejected cookies in settings → cache purged; (d) drop-in serves dynamic page when rejected cookie is present.Out of Scope
DONOTCACHEPAGEconstant handling.Further Notes
wordpress_*wildcard in defaults subsumes the existing explicitwordpress_logged_incheck in the drop-in. The explicit check can remain as a fast-path without harm, or be removed once the wildcard is proven — a decision for implementation time.docs/plans/2026-09-13-wprecache-design.md) notes the drop-in cannot read WP options. This spec respects that constraint by baking the option value into the generated file.