Skip to content

Validated Stripe price ID before checkout - #2979

Merged
poonam279 merged 5 commits into
developmentfrom
bugfix/otter-internal/311
Aug 20, 2026
Merged

Validated Stripe price ID before checkout#2979
poonam279 merged 5 commits into
developmentfrom
bugfix/otter-internal/311

Conversation

@girishpanchal30

Copy link
Copy Markdown
Contributor

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

  • Included E2E or unit tests for the changes in this PR.
  • Visual elements are not affected by independent changes.
  • It is at least compatible with the minimum WordPress version.
  • It loads additional script in frontend only if it is required.
  • Does not impact the Core Web Vitals.
  • In case of deprecation, old blocks are safely migrated.
  • It is usable in Widgets and FSE.
  • Copy/Paste is working if the attributes are modified.
  • PR is following the best practices

@pirate-bot

Copy link
Copy Markdown
Contributor

Bundle Size Diff

Package Old Size New Size Diff
Animations 178.33 KB 178.33 KB 0 B (0.00%)
Blocks 1.65 MB 1.65 MB 0 B (0.00%)
CSS 7.83 KB 7.83 KB 0 B (0.00%)
Dashboard 172.49 KB 172.49 KB 0 B (0.00%)
Onboarding 68.14 KB 68.14 KB 0 B (0.00%)
Export Import 4.73 KB 4.73 KB 0 B (0.00%)
Pro 439.82 KB 439.82 KB 0 B (0.00%)

@pirate-bot

pirate-bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Plugin build for 4533331 is ready 🛎️!

@pirate-bot

pirate-bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

E2E Tests

Playwright Test Status: See serial and parallel matrix jobs

Performance Results serverResponse: {"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}

Copilot AI 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.

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 from render() 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_status is considered. A caller who knows the product and price IDs can therefore create a session without supplying the page password. Also reject the post while post_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_block posts. WordPress does not render such a core/block reference, 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 (see inc/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.

Comment thread inc/render/class-stripe-checkout-block.php Outdated
Comment thread inc/render/class-stripe-checkout-block.php Outdated

Copilot AI 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.

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 in set_up() and restore them in tear_down() (e.g., restore the previous Stripe HTTP client, and restore/delete the themeisle_stripe_api_key option).
		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 in set_up() and restore them in tear_down() (e.g., restore the previous Stripe HTTP client, and restore/delete the themeisle_stripe_api_key option).
	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 to watch_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 mode adds an extra Stripe API call (price fetch) 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 per price_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 mode adds an extra Stripe API call (price fetch) 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 per price_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 );

Copilot stopped reviewing on behalf of girishpanchal30 due to an error August 19, 2026 07:14

Copilot AI 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.

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_time price, so the existing subscription checkout path is unverified after removing the client-supplied mode. Add a recurring-price fixture and assert that checkout uses/caches subscription; 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 selul 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.

The origin check only covers blocks stored in the resolved post's post_content, which breaks legitimate placements.

Comment thread inc/render/class-stripe-checkout-block.php Outdated
@poonam279
poonam279 merged commit b749aa4 into development Aug 20, 2026
15 checks passed
@poonam279
poonam279 deleted the bugfix/otter-internal/311 branch August 20, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants