Validated Stripe price ID before checkout - #2979
Conversation
Bundle Size Diff
|
|
Plugin build for 4533331 is ready 🛎️!
|
E2E TestsPlaywright Test Status: See serial and parallel matrix jobs Performance ResultsserverResponse: {"q25":471.63,"q50":475.17,"q75":477.5,"cnt":10}, firstPaint: {"q25":1667.25,"q50":1844.66,"q75":2089.6,"cnt":10}, domContentLoaded: {"q25":4259.85,"q50":4298.83,"q75":4320.31,"cnt":10}, loaded: {"q25":4263.74,"q50":4302.43,"q75":4324.33,"cnt":10}, firstContentfulPaint: {"q25":2766.48,"q50":3211.08,"q75":3245.35,"cnt":10}, firstBlock: {"q25":19869.29,"q50":20011.42,"q75":20078.29,"cnt":10}, type: {"q25":30.74,"q50":36.29,"q75":42.3,"cnt":10}, typeWithoutInspector: {"q25":27.97,"q50":30.43,"q75":38.41,"cnt":10}, typeWithTopToolbar: {"q25":42.35,"q50":45.37,"q75":47.81,"cnt":10}, typeContainer: {"q25":16.88,"q50":17.52,"q75":19.69,"cnt":10}, focus: {"q25":158.61,"q50":163.86,"q75":176.31,"cnt":10}, inserterOpen: {"q25":53.18,"q50":55.93,"q75":58.02,"cnt":10}, inserterSearch: {"q25":33.86,"q50":35.88,"q75":37.62,"cnt":10}, inserterHover: {"q25":5.95,"q50":6.21,"q75":6.64,"cnt":20}, loadPatterns: {"q25":2136.92,"q50":2163.74,"q75":2290.99,"cnt":10}, listViewOpen: {"q25":284.69,"q50":292.56,"q75":307.63,"cnt":10} |
There was a problem hiding this comment.
Pull request overview
Validates Stripe checkout requests against the product and price configured on the source page.
Changes:
- Resolves and inspects the checkout source post.
- Derives Stripe session mode server-side.
- Adds checkout validation tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
inc/render/class-stripe-checkout-block.php |
Adds checkout source validation and mode resolution. |
tests/test-stripe-checkout-block.php |
Tests accepted and rejected checkout requests. |
Suppressed comments (3)
inc/render/class-stripe-checkout-block.php:225
- This validation only searches the resolved post's
post_content. A checkout block rendered from an FSE template/template part or a block widget still builds its link from the current permalink, but it is not stored in that post, so every checkout click is rejected. This contradicts the checked Widgets/FSE compatibility; validate the actual render source or emit a server-verifiable token fromrender()instead of relying solely on post content.
return $this->blocks_have_checkout( parse_blocks( $post->post_content ), $product_id, $price_id ) ? $post_id : 0;
inc/render/class-stripe-checkout-block.php:223
- Published password-protected posts pass this check for anonymous requests because only
post_statusis considered. A caller who knows the product and price IDs can therefore create a session without supplying the page password. Also reject the post whilepost_password_required( $post )is true.
if ( 'publish' !== $post->post_status && ! current_user_can( 'read_post', $post_id ) ) {
return 0;
}
inc/render/class-stripe-checkout-block.php:260
- This accepts content from any referenced
WP_Post, including draft, trashed, password-protected, or non-wp_blockposts. WordPress does not render such acore/blockreference, so stale/hand-authored references can authorize a product/price pair that is not actually offered on the page. Apply the same post-type/status/password checks used when rendering reusable blocks (seeinc/class-blocks-css.php:192-197).
$reusable = get_post( (int) $block['attrs']['ref'] );
if ( $reusable instanceof \WP_Post && $this->blocks_have_checkout( parse_blocks( $reusable->post_content ), $product_id, $price_id, $depth + 1 ) ) {
return true;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (5)
tests/test-stripe-checkout-block.php:37
- The test mutates global/shared state (a WP option and Stripe’s global HTTP client) but doesn’t restore it in
tear_down(). This can make the test suite order-dependent and cause unrelated tests to use the mock client or test API key. Store previous values inset_up()and restore them intear_down()(e.g., restore the previous Stripe HTTP client, and restore/delete thethemeisle_stripe_api_keyoption).
update_option( 'themeisle_stripe_api_key', 'sk_test' );
\Stripe\ApiRequestor::setHttpClient( new StripeHttpClientMock() );
tests/test-stripe-checkout-block.php:59
- The test mutates global/shared state (a WP option and Stripe’s global HTTP client) but doesn’t restore it in
tear_down(). This can make the test suite order-dependent and cause unrelated tests to use the mock client or test API key. Store previous values inset_up()and restore them intear_down()(e.g., restore the previous Stripe HTTP client, and restore/delete thethemeisle_stripe_api_keyoption).
public function tear_down() {
remove_filter( 'wp_redirect', array( $this, 'throw_on_redirect' ) );
unset( $_GET['action'], $_GET['product_id'], $_GET['price_id'], $_GET['url'] );
parent::tear_down();
inc/render/class-stripe-checkout-block.php:287
- If fetching the Stripe price fails (WP_Error) or returns an unexpected shape, this falls back to
'payment'. For recurring prices, that likely creates an invalid Checkout Session request (or a hard-to-diagnose checkout failure). Consider propagating the failure towatch_checkout()(e.g., return a WP_Error or sentinel) and show an error instead of guessing the mode.
private function get_mode_for_price( $price_id ) {
$price = $this->stripe_api->create_request( 'price', $price_id );
if ( is_wp_error( $price ) || ! isset( $price['type'] ) ) {
return 'payment';
}
return 'recurring' === $price['type'] ? 'subscription' : 'payment';
}
inc/render/class-stripe-checkout-block.php:96
- Deriving
modeadds an extra Stripe API call (pricefetch) for every checkout request, on top of the Checkout Session creation call. To reduce latency and Stripe API usage, consider caching the price type/mode perprice_id(e.g., in a transient/object cache with a reasonable TTL) and only refetch when the cache is cold.
'quantity' => 1,
),
),
'mode' => $this->get_mode_for_price( $price_id ),
inc/render/class-stripe-checkout-block.php:288
- Deriving
modeadds an extra Stripe API call (pricefetch) for every checkout request, on top of the Checkout Session creation call. To reduce latency and Stripe API usage, consider caching the price type/mode perprice_id(e.g., in a transient/object cache with a reasonable TTL) and only refetch when the cache is cold.
private function get_mode_for_price( $price_id ) {
$price = $this->stripe_api->create_request( 'price', $price_id );
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
inc/render/class-stripe-checkout-block.php:234
- This lookup only scans the queried post's saved
post_content. If the checkout block is rendered from a block-theme template/template part or a block widget,get_permalink()still points to the current page, but that page's content does not contain the block, so every valid checkout click is rejected. This breaks the PR checklist's FSE/widgets compatibility; validate against the rendered source (or a server-verifiable token generated from the rendered attributes) rather than only the queried post.
$visited = array( $post_id => true );
return $this->blocks_have_checkout( parse_blocks( $post->post_content ), $product_id, $price_id, $visited ) ? $post_id : 0;
inc/render/class-stripe-checkout-block.php:304
- The new server-side mode branch is only tested with the mock's
one_timeprice, so the existing subscription checkout path is unverified after removing the client-supplied mode. Add a recurring-price fixture and assert that checkout uses/cachessubscription; otherwise a regression here would make all recurring prices fail at Stripe.
$mode = 'recurring' === $price['type'] ? 'subscription' : 'payment';
set_transient( $cache_key, $mode, WEEK_IN_SECONDS );
selul
left a comment
There was a problem hiding this comment.
The origin check only covers blocks stored in the resolved post's post_content, which breaks legitimate placements.
Closes https://github.com/Codeinwp/otter-internals/issues/311
Summary
Compared the price ID and product ID from the URL with the current product and price IDs from the block attributes. If they differ, we return
0, indicating that checkout is not available on the current page.Checklist before the final review