Commit 371014d75e8 for woocommerce
commit 371014d75e84fc61e444f8e5c9eece8488836832
Author: Ján Mikláš <neosinner@gmail.com>
Date: Wed Jul 8 11:04:43 2026 +0200
Validate mail callback return type (#66346)
* Validate mail callback return type
* Coerce scalar mail callback returns to bool instead of forcing false
diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php
index fa15ee82766..475b4f450e7 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email.php
@@ -1233,7 +1233,21 @@ class WC_Email extends WC_Settings_API {
$message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
$mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
$mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this );
- $return = (bool) $mail_callback( ...$mail_callback_params );
+ $return = $mail_callback( ...$mail_callback_params );
+ if ( ! is_bool( $return ) ) {
+ $original_type = gettype( $return );
+
+ $return = is_scalar( $return ) ? wc_string_to_bool( (string) $return ) : false;
+
+ wc_doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ 'The callback registered to the woocommerce_mail_callback filter should return a boolean; %s returned.',
+ $original_type
+ ),
+ '11.1.0'
+ );
+ }
remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
index 5a7e746a376..33084fa7503 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
@@ -910,23 +910,101 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
}
/**
- * @testdox send() coerces a non-bool return from the mail callback into a bool.
+ * @testdox send() returns false and warns when the mail callback returns a non-scalar.
*/
- public function test_send_coerces_non_bool_mail_callback_return_to_bool(): void {
+ public function test_send_returns_false_and_warns_when_mail_callback_returns_non_scalar(): void {
add_filter(
'woocommerce_mail_callback',
function () {
- return static function () {
- return null;
- };
+ return static function () {
+ return new \WP_Error( 'failed', 'Mail failed' );
+ };
}
);
+ add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ $this->setExpectedIncorrectUsage( 'WC_Email::send' );
+
+ $notices = array();
+ $action = function ( $function_name, $message, $version ) use ( &$notices ) {
+ $notices[] = array(
+ 'function_name' => $function_name,
+ 'message' => $message,
+ 'version' => $version,
+ );
+ };
+ add_action( 'doing_it_wrong_run', $action, 10, 3 );
- $email = new \WC_Email();
- $result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+ try {
+ $email = new \WC_Email();
+ $result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+ } finally {
+ remove_action( 'doing_it_wrong_run', $action, 10 );
+ remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ }
- $this->assertIsBool( $result, 'send() should always return a bool, even if the mail callback does not' );
- $this->assertFalse( $result, 'A non-bool (null) callback return should coerce to false' );
+ $this->assertFalse( $result, 'A non-scalar callback return should resolve to false' );
+ $this->assertNotEmpty( $notices );
+ $this->assertSame( 'WC_Email::send', $notices[0]['function_name'] );
+ $this->assertStringContainsString( 'woocommerce_mail_callback filter should return a boolean; object returned.', $notices[0]['message'] );
+ $this->assertSame( '11.1.0', $notices[0]['version'] );
+ }
+
+ /**
+ * @testdox send() coerces a scalar mail callback return to bool and warns.
+ *
+ * @dataProvider provider_scalar_callback_returns
+ *
+ * @param mixed $callback_return Value returned by the mail callback.
+ * @param bool $expected Expected coerced send result.
+ * @param string $expected_type gettype() of the returned value, as reported in the notice.
+ */
+ public function test_send_coerces_scalar_callback_return_and_warns( $callback_return, bool $expected, string $expected_type ): void {
+ add_filter(
+ 'woocommerce_mail_callback',
+ function () use ( $callback_return ) {
+ return static function () use ( $callback_return ) {
+ return $callback_return;
+ };
+ }
+ );
+ add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ $this->setExpectedIncorrectUsage( 'WC_Email::send' );
+
+ $notices = array();
+ $action = function ( $function_name, $message, $version ) use ( &$notices ) {
+ $notices[] = array(
+ 'function_name' => $function_name,
+ 'message' => $message,
+ 'version' => $version,
+ );
+ };
+ add_action( 'doing_it_wrong_run', $action, 10, 3 );
+
+ try {
+ $email = new \WC_Email();
+ $result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+ } finally {
+ remove_action( 'doing_it_wrong_run', $action, 10 );
+ remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ }
+
+ $this->assertSame( $expected, $result );
+ $this->assertNotEmpty( $notices );
+ $this->assertStringContainsString( "woocommerce_mail_callback filter should return a boolean; {$expected_type} returned.", $notices[0]['message'] );
+ }
+
+ /**
+ * Scalar mail callback return values and their expected coercion.
+ *
+ * @return array<string, array<mixed>>
+ */
+ public function provider_scalar_callback_returns(): array {
+ return array(
+ 'integer 1 is a success' => array( 1, true, 'integer' ),
+ 'string yes is a success' => array( 'yes', true, 'string' ),
+ 'integer 0 is a failure' => array( 0, false, 'integer' ),
+ 'random string is a failure' => array( 'sent', false, 'string' ),
+ );
}
/**
@@ -936,32 +1014,40 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
add_filter(
'woocommerce_mail_callback',
function () {
- return static function () {
- return null;
- };
+ return static function () {
+ return new \WP_Error( 'failed', 'Mail failed' );
+ };
}
);
+ add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ $this->setExpectedIncorrectUsage( 'WC_Email::send' );
$email = $this->create_testable_email( 'my_email', 'admin@example.com', true, false, true );
- $result = $email->run_send_notification();
+ try {
+ $result = $email->run_send_notification();
+ } finally {
+ remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ }
$this->assertIsBool( $result, 'send_notification() should return a bool instead of fataling' );
- $this->assertFalse( $result, 'Should resolve to false when the underlying mail callback returns null' );
+ $this->assertFalse( $result, 'Should resolve to false when the underlying mail callback returns a non-bool' );
}
/**
- * @testdox send() casts the mail callback return to bool before firing woocommerce_email_sent.
+ * @testdox send() passes false to woocommerce_email_sent when the mail callback returns a non-bool.
*/
public function test_send_fires_email_sent_action_with_bool_when_callback_returns_non_bool(): void {
add_filter(
'woocommerce_mail_callback',
function () {
- return static function () {
- return null;
- };
+ return static function () {
+ return new \WP_Error( 'failed', 'Mail failed' );
+ };
}
);
+ add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ $this->setExpectedIncorrectUsage( 'WC_Email::send' );
$received_return = null;
add_action(
@@ -974,9 +1060,13 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
);
$email = new \WC_Email();
- $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+ try {
+ $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+ } finally {
+ remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+ }
- $this->assertIsBool( $received_return, 'woocommerce_email_sent should receive a bool even when the mail callback returns null' );
+ $this->assertIsBool( $received_return, 'woocommerce_email_sent should receive a bool even when the mail callback returns a non-bool' );
$this->assertFalse( $received_return );
remove_all_actions( 'woocommerce_email_sent' );