Commit ba67e4c924e for woocommerce

commit ba67e4c924e4fd149b233b3ade560cfda88d2471
Author: Yuliyan Slavchev <yuliyan.slavchev@gmail.com>
Date:   Mon Feb 23 21:06:22 2026 +0200

    Email Editor: Check edit permission for post in send preview email (#63413)

    * Email Editor: Check edit permission for post in send preview email

    * Add changelog

    * Add tests

    * Fix lint and phpstan

diff --git a/packages/php/email-editor/changelog/wooplug-6338-improve-permission-checks-for-send-preview-email-endpoint b/packages/php/email-editor/changelog/wooplug-6338-improve-permission-checks-for-send-preview-email-endpoint
new file mode 100644
index 00000000000..4ff22ca47bf
--- /dev/null
+++ b/packages/php/email-editor/changelog/wooplug-6338-improve-permission-checks-for-send-preview-email-endpoint
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Verify user can edit post in send preview email endpoint permission check
diff --git a/packages/php/email-editor/src/Engine/class-email-editor.php b/packages/php/email-editor/src/Engine/class-email-editor.php
index 6363a0e448c..3b26a1f95b0 100644
--- a/packages/php/email-editor/src/Engine/class-email-editor.php
+++ b/packages/php/email-editor/src/Engine/class-email-editor.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalizatio
 use Automattic\WooCommerce\EmailEditor\Engine\Templates\Templates;
 use Automattic\WooCommerce\EmailEditor\Engine\Logger\Email_Editor_Logger;
 use WP_Post;
+use WP_REST_Request;
 use WP_Theme_JSON;

 /**
@@ -263,8 +264,15 @@ class Email_Editor {
 			array(
 				'methods'             => 'POST',
 				'callback'            => array( $this->email_api_controller, 'send_preview_email_data' ),
-				'permission_callback' => function () {
-					return current_user_can( 'edit_posts' );
+				'permission_callback' => function ( WP_REST_Request $request ) {
+					if ( ! current_user_can( 'edit_posts' ) ) {
+						return false;
+					}
+					$post_id = $request->get_param( 'postId' );
+					if ( ! is_numeric( $post_id ) || (int) $post_id <= 0 ) {
+						return false;
+					}
+					return current_user_can( 'edit_post', (int) $post_id );
 				},
 			)
 		);
diff --git a/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php
new file mode 100644
index 00000000000..076e101fe03
--- /dev/null
+++ b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php
@@ -0,0 +1,301 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare(strict_types = 1);
+namespace Automattic\WooCommerce\EmailEditor\Engine;
+
+/**
+ * Integration test for the send_preview_email endpoint permission callback.
+ */
+class Send_Preview_Email_Permission_Test extends \Email_Editor_Integration_Test_Case {
+
+	/**
+	 * Email editor instance.
+	 *
+	 * @var Email_Editor
+	 */
+	private $email_editor;
+
+	/**
+	 * REST server instance.
+	 *
+	 * @var \WP_REST_Server
+	 */
+	private $server;
+
+	/**
+	 * The endpoint route.
+	 *
+	 * @var string
+	 */
+	private const ROUTE = '/woocommerce-email-editor/v1/send_preview_email';
+
+	/**
+	 * Creates a user and returns an integer ID.
+	 *
+	 * @param array $args Arguments for user creation.
+	 * @return int
+	 */
+	private function create_user( array $args ): int {
+		$result = self::factory()->user->create( $args );
+		$this->assertIsInt( $result );
+		return $result;
+	}
+
+	/**
+	 * Creates a post and returns an integer ID.
+	 *
+	 * @param array $args Arguments for post creation.
+	 * @return int
+	 */
+	private function create_post( array $args = array() ): int {
+		$result = self::factory()->post->create( $args );
+		$this->assertIsInt( $result );
+		return $result;
+	}
+
+	/**
+	 * Set up before each test.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->email_editor = $this->di_container->get( Email_Editor::class );
+
+		global $wp_rest_server;
+		$wp_rest_server = new \WP_REST_Server();
+		$this->server   = $wp_rest_server;
+
+		do_action( 'rest_api_init' );
+		$this->email_editor->register_email_editor_api_routes();
+	}
+
+	/**
+	 * Clean up after each test.
+	 */
+	public function tearDown(): void {
+		parent::tearDown();
+		global $wp_rest_server;
+		$wp_rest_server = null;
+	}
+
+	/**
+	 * Test that an admin can send a preview email for their own post.
+	 */
+	public function testAdminCanSendPreviewForOwnPost(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		$post_id  = $this->create_post( array( 'post_author' => $admin_id ) );
+
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertNotEquals( 403, $response->get_status(), 'Admin should be allowed to send preview for own post' );
+	}
+
+	/**
+	 * Test that an editor can send a preview email for another user's post.
+	 */
+	public function testEditorCanSendPreviewForOtherUsersPost(): void {
+		$author_id = $this->create_user( array( 'role' => 'author' ) );
+		$editor_id = $this->create_user( array( 'role' => 'editor' ) );
+		$post_id   = $this->create_post( array( 'post_author' => $author_id ) );
+
+		wp_set_current_user( $editor_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertNotEquals( 403, $response->get_status(), 'Editor should be allowed to send preview for another user\'s post' );
+	}
+
+	/**
+	 * Test that a contributor cannot send a preview email for another user's private post.
+	 */
+	public function testContributorCannotSendPreviewForOtherUsersPrivatePost(): void {
+		$admin_id       = $this->create_user( array( 'role' => 'administrator' ) );
+		$contributor_id = $this->create_user( array( 'role' => 'contributor' ) );
+		$post_id        = $this->create_post(
+			array(
+				'post_author' => $admin_id,
+				'post_status' => 'private',
+			)
+		);
+
+		wp_set_current_user( $contributor_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Contributor should not be allowed to send preview for another user\'s private post' );
+	}
+
+	/**
+	 * Test that a subscriber cannot send a preview email.
+	 */
+	public function testSubscriberCannotSendPreview(): void {
+		$admin_id      = $this->create_user( array( 'role' => 'administrator' ) );
+		$subscriber_id = $this->create_user( array( 'role' => 'subscriber' ) );
+		$post_id       = $this->create_post( array( 'post_author' => $admin_id ) );
+
+		wp_set_current_user( $subscriber_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Subscriber should not be allowed to send preview email' );
+	}
+
+	/**
+	 * Test that a request without postId is denied.
+	 */
+	public function testRequestWithoutPostIdIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email' => 'test@example.com',
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Request without postId should be denied' );
+	}
+
+	/**
+	 * Test that a request with non-numeric postId is denied.
+	 */
+	public function testRequestWithNonNumericPostIdIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => 'abc',
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Request with non-numeric postId should be denied' );
+	}
+
+	/**
+	 * Test that a request with zero postId is denied.
+	 */
+	public function testRequestWithZeroPostIdIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => 0,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Request with zero postId should be denied' );
+	}
+
+	/**
+	 * Test that a request with negative postId is denied.
+	 */
+	public function testRequestWithNegativePostIdIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => -1,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Request with negative postId should be denied' );
+	}
+
+	/**
+	 * Test that an unauthenticated request is denied.
+	 */
+	public function testUnauthenticatedRequestIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		$post_id  = $this->create_post( array( 'post_author' => $admin_id ) );
+
+		wp_set_current_user( 0 );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertContains( $response->get_status(), array( 401, 403 ), 'Unauthenticated request should be denied' );
+	}
+
+	/**
+	 * Test that a request with non-existent postId is denied.
+	 */
+	public function testRequestWithNonExistentPostIdIsDenied(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => 999999,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Request with non-existent postId should be denied' );
+	}
+}