Skip to content

fix(checkout): prevent caching live checkout state#1354

Merged
superdav42 merged 1 commit into
mainfrom
fix/issue-1330-checkout-cache
Jun 8, 2026
Merged

fix(checkout): prevent caching live checkout state#1354
superdav42 merged 1 commit into
mainfrom
fix/issue-1330-checkout-cache

Conversation

@superdav42

@superdav42 superdav42 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add explicit live-checkout no-cache protection in Checkout_Element::setup() and Checkout_Element::output().
  • Send cache-bypass signals for common WordPress, reverse-proxy, CDN, and LiteSpeed configurations.
  • Add optional deferred checkout rendering (defer / defer_trigger) so marketing pages can stay cacheable until visitor intent, then fetch fresh checkout markup via light-AJAX and set wu_checkout_intent.
  • Add regression coverage for deferred defaults, placeholder rendering, and live checkout cache safeguards.

Research notes

Checked public cache implementation patterns for:

  • DONOTCACHEPAGE compatibility used by WordPress page-cache plugins.
  • nocache_headers() plus explicit Cache-Control: no-store for stateful responses.
  • LiteSpeed litespeed_control_set_nocache bypass action and cache-control header.
  • Nginx/reverse-proxy X-Accel-Expires: 0.
  • CDN/surrogate CDN-Cache-Control and Surrogate-Control no-store signals.

Verification

  • vendor/bin/phpcs inc/ui/class-checkout-element.php tests/WP_Ultimo/UI/Checkout_Element_Test.php
  • vendor/bin/phpunit --filter Checkout_Element_Test
  • vendor/bin/phpstan analyse inc/ui/class-checkout-element.php tests/WP_Ultimo/UI/Checkout_Element_Test.php
  • git diff --check

Resolves #1330


aidevops.sh v3.20.36 plugin for OpenCode v1.16.2 with gpt-5.5

Summary by CodeRabbit

  • New Features

    • Added deferred checkout support with configurable triggers (click, viewport, page-load) for improved cache compatibility and page performance.
    • Checkout elements can now render as lightweight placeholders and load live content dynamically via AJAX when user interaction occurs.
  • Tests

    • Added comprehensive test coverage validating deferred checkout configuration, cache safety mechanisms, and dynamic content loading behavior.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements deferred/lazy checkout rendering with cache safety. Checkout_Element now supports an optional defer mode controlled by Gutenberg UI settings, sends no-cache headers when rendering live checkout, and provides an AJAX endpoint to fetch fresh checkout state on demand. A cache-safe placeholder with client-side JavaScript trigger logic allows initial page cache while ensuring checkout interactions always receive fresh per-request state.

Changes

Deferred checkout with cache safety

Layer / File(s) Summary
Defer settings and configuration
inc/ui/class-checkout-element.php
Added defer toggle and defer_trigger select (viewport/click/load) to Gutenberg fields, registered AJAX actions, and introduced corresponding defaults.
Cache safety and no-cache headers
inc/ui/class-checkout-element.php
Implemented send_checkout_nocache_headers() to define DONOTCACHEPAGE and set no-cache headers; integrated into setup() and enqueue_element_scripts() to send headers only for live checkout and skip enqueue for deferred placeholders.
AJAX endpoint and request handling
inc/ui/class-checkout-element.php
Implemented render_deferred_checkout() AJAX handler with sanitized request parsing, intent cookie setting, live checkout rendering, and JSON response; added helper methods for request sanitization, cookie persistence, and referer bypass.
Intent detection and deferred placeholder rendering
inc/ui/class-checkout-element.php
Implemented intent detection and deferral decision logic, trigger normalization, and output_deferred_placeholder() with inline JavaScript for client-side trigger handling (viewport IntersectionObserver, click, or page-load), AJAX request, HTML injection, script replacement, and error display; modified output() to conditionally defer.
Test coverage for deferred checkout
tests/WP_Ultimo/UI/Checkout_Element_Test.php
Added Checkout_Element_Test class with tests for defer/defer_trigger defaults, Gutenberg field definitions, deferred placeholder output rendering, and assertions that live checkout path includes explicit no-cache safeguards.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

review-feedback-scanned, status:in-review

Poem

🐰 A checkout that defers with grace,
Lets marketing pages cache their face,
While fresh state waits for user's call,
No-cache guards protect it all,
Sweet placeholder, then the dance—
Intent fetches checkout's chance!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(checkout): prevent caching live checkout state' directly aligns with the main objective: adding explicit no-cache protection to prevent full-page caches from storing stateful live checkout HTML. This is the primary Phase 1 goal in issue #1330.
Linked Issues check ✅ Passed The pull request implements both Phase 1 (live checkout no-cache signals via setup()/output()) and Phase 2 (deferred checkout with AJAX/placeholder) from issue #1330, including all acceptance criteria: no-cache headers, DONOTCACHEPAGE, cache-bypass signals, deferred mode with intent detection, and test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #1330: the Checkout_Element class receives cache-safety logic and deferred checkout support, and tests validate the implementation. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1330-checkout-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
inc/ui/class-checkout-element.php (1)

536-540: 💤 Low value

sanitize_key() lowercases header names, which changes the intended casing.

sanitize_key() converts strings to lowercase and removes non-alphanumeric characters except dashes and underscores. This means 'Cache-Control' becomes 'cache-control', 'X-Accel-Expires' becomes 'x-accel-expires', etc. While HTTP/1.1 headers are case-insensitive, HTTP/2 requires lowercase headers. However, PHP's header() function typically handles this correctly regardless. The behavior is acceptable but worth noting for documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/ui/class-checkout-element.php` around lines 536 - 540, The code is
lowercasing header names by calling sanitize_key() before sending them; remove
sanitize_key() and send header names as provided (after trimming CR/LF), or
alternatively validate/whitelist header names without altering case to preserve
original casing; update the foreach block that iterates $headers (and the
header(...) call) to use the original $name (or a validated/whitelisted version)
instead of sanitize_key($name) while still sanitizing $value and preventing CRLF
injection.
tests/WP_Ultimo/UI/Checkout_Element_Test.php (1)

87-99: 💤 Low value

Source-string assertions are brittle but serve as regression guardrails.

This test reads the source file to verify cache-safety patterns exist. While unconventional, it effectively prevents accidental removal of critical no-cache mechanisms. The path dirname(__DIR__, 3) . '/inc/ui/class-checkout-element.php' resolves correctly from tests/WP_Ultimo/UI/ → project root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WP_Ultimo/UI/Checkout_Element_Test.php` around lines 87 - 99, The test
test_source_contains_live_checkout_cache_safeguards() intentionally reads the
source file to assert the presence of cache-safety tokens; keep this test but
add an inline explanation comment and ensure it uses the same path resolution
(dirname(__DIR__, 3) . '/inc/ui/class-checkout-element.php'), retains the phpcs
ignore, and preserves assertions for 'DONOTCACHEPAGE', 'nocache_headers()',
'wu_checkout_nocache_required', 'litespeed_control_set_nocache',
'X-Accel-Expires', 'CDN-Cache-Control', 'Surrogate-Control', and
'X-LiteSpeed-Cache-Control' so the regression guardrails remain in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/ui/class-checkout-element.php`:
- Around line 866-873: The viewport auto-load branch currently only runs when
'IntersectionObserver' exists, leaving older browsers without a fallback; update
the block that checks "else if ('viewport' === trigger && 'IntersectionObserver'
in window)" to handle the else case by attaching a safe fallback that calls
loadCheckout() when the document becomes visible (e.g., on DOMContentLoaded or
on first scroll/resize) and then removes its listener(s); reference the trigger
variable, loadCheckout(), root and the IntersectionObserver usage so you add a
fallback path that triggers loadCheckout() once and cleans up listeners just
like the observer.disconnect() path.

---

Nitpick comments:
In `@inc/ui/class-checkout-element.php`:
- Around line 536-540: The code is lowercasing header names by calling
sanitize_key() before sending them; remove sanitize_key() and send header names
as provided (after trimming CR/LF), or alternatively validate/whitelist header
names without altering case to preserve original casing; update the foreach
block that iterates $headers (and the header(...) call) to use the original
$name (or a validated/whitelisted version) instead of sanitize_key($name) while
still sanitizing $value and preventing CRLF injection.

In `@tests/WP_Ultimo/UI/Checkout_Element_Test.php`:
- Around line 87-99: The test
test_source_contains_live_checkout_cache_safeguards() intentionally reads the
source file to assert the presence of cache-safety tokens; keep this test but
add an inline explanation comment and ensure it uses the same path resolution
(dirname(__DIR__, 3) . '/inc/ui/class-checkout-element.php'), retains the phpcs
ignore, and preserves assertions for 'DONOTCACHEPAGE', 'nocache_headers()',
'wu_checkout_nocache_required', 'litespeed_control_set_nocache',
'X-Accel-Expires', 'CDN-Cache-Control', 'Surrogate-Control', and
'X-LiteSpeed-Cache-Control' so the regression guardrails remain in place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a43df532-2d4f-4b7c-a493-9ff3ea163d51

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb56ab and 712ec84.

📒 Files selected for processing (2)
  • inc/ui/class-checkout-element.php
  • tests/WP_Ultimo/UI/Checkout_Element_Test.php

Comment on lines +866 to +873
} else if ('viewport' === trigger && 'IntersectionObserver' in window) {
new IntersectionObserver(function(entries, observer) {
if (entries.some(function(entry) { return entry.isIntersecting; })) {
observer.disconnect();
loadCheckout();
}
}).observe(root);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No fallback when IntersectionObserver is unavailable for viewport trigger.

When trigger === 'viewport' but IntersectionObserver is not supported (e.g., older browsers), the checkout won't auto-load. The button click and focusin handlers remain active, so users can still manually trigger checkout. Consider adding a fallback (e.g., load on DOMContentLoaded) for browsers without IntersectionObserver.

Proposed fix to add fallback for older browsers
 			} else if ('viewport' === trigger && 'IntersectionObserver' in window) {
 				new IntersectionObserver(function(entries, observer) {
 					if (entries.some(function(entry) { return entry.isIntersecting; })) {
 						observer.disconnect();
 						loadCheckout();
 					}
 				}).observe(root);
+			} else if ('viewport' === trigger) {
+				// Fallback for browsers without IntersectionObserver: load on DOMContentLoaded
+				if ('loading' === document.readyState) {
+					document.addEventListener('DOMContentLoaded', loadCheckout);
+				} else {
+					loadCheckout();
+				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if ('viewport' === trigger && 'IntersectionObserver' in window) {
new IntersectionObserver(function(entries, observer) {
if (entries.some(function(entry) { return entry.isIntersecting; })) {
observer.disconnect();
loadCheckout();
}
}).observe(root);
}
} else if ('viewport' === trigger && 'IntersectionObserver' in window) {
new IntersectionObserver(function(entries, observer) {
if (entries.some(function(entry) { return entry.isIntersecting; })) {
observer.disconnect();
loadCheckout();
}
}).observe(root);
} else if ('viewport' === trigger) {
// Fallback for browsers without IntersectionObserver: load on DOMContentLoaded
if ('loading' === document.readyState) {
document.addEventListener('DOMContentLoaded', loadCheckout);
} else {
loadCheckout();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/ui/class-checkout-element.php` around lines 866 - 873, The viewport
auto-load branch currently only runs when 'IntersectionObserver' exists, leaving
older browsers without a fallback; update the block that checks "else if
('viewport' === trigger && 'IntersectionObserver' in window)" to handle the else
case by attaching a safe fallback that calls loadCheckout() when the document
becomes visible (e.g., on DOMContentLoaded or on first scroll/resize) and then
removes its listener(s); reference the trigger variable, loadCheckout(), root
and the IntersectionObserver usage so you add a fallback path that triggers
loadCheckout() once and cleans up listeners just like the observer.disconnect()
path.

@superdav42 superdav42 merged commit 4c2fd3d into main Jun 8, 2026
11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Summary

  • Add explicit live-checkout no-cache protection in Checkout_Element::setup() and Checkout_Element::output().
  • Send cache-bypass signals for common WordPress, reverse-proxy, CDN, and LiteSpeed configurations.
  • Add optional deferred checkout rendering (defer / defer_trigger) so marketing pages can stay cacheable until visitor intent, then fetch fresh checkout markup via light-AJAX and set wu_checkout_intent.
  • Add regression coverage for deferred defaults, placeholder rendering, and live checkout cache safeguards.

Research notes

Checked public cache implementation patterns for:

  • DONOTCACHEPAGE compatibility used by WordPress page-cache plugins.
  • nocache_headers() plus explicit Cache-Control: no-store for stateful responses.
  • LiteSpeed litespeed_control_set_nocache bypass action and cache-control header.
  • Nginx/reverse-proxy X-Accel-Expires: 0.
  • CDN/surrogate CDN-Cache-Control and Surrogate-Control no-store signals.

Verification

  • vendor/bin/phpcs inc/ui/class-checkout-element.php tests/WP_Ultimo/UI/Checkout_Element_Test.php
  • vendor/bin/phpunit --filter Checkout_Element_Test
  • vendor/bin/phpstan analyse inc/ui/class-checkout-element.php tests/WP_Ultimo/UI/Checkout_Element_Test.php
  • git diff --check

aidevops.sh v3.20.36 plugin for OpenCode v1.16.2 with gpt-5.5


Merged via PR #1354 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent full-page cache from storing live checkout shortcode/block state

1 participant