Commit fcb087f9dee for woocommerce
commit fcb087f9dee1225be7a4a04840bab5f6163bad70
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Wed Sep 2 13:53:27 2026 -0400
Fix sub-1 review ratings and replace legacy WPCS suppressions (#68268)
Product review ratings posted through the core comment form were
guarded with `! $rating || $rating > 5 || $rating < 0` and then stored
with intval(). A truthy fractional value below one, such as 0.5, passed
every check and was written as a rating of 0. The rating is now read
once, range-checked as a number before truncation so 0.5 and 5.9 are
both rejected, then bounded to 1-5 as an integer. Whole and in-range
decimal ratings store exactly what they did before (4.7 still stores 4).
Stored zeros were already ignored by every reader, so no displayed
average, count or structured data changes; a junk meta row is no longer
written.
The legacy `// WPCS: ... ok.` comments in class-wc-comments.php,
class-wc-background-emailer.php and wc-cart-functions.php are not
honoured by WPCS 3.x. Each is replaced with a line-scoped phpcs:ignore
naming exactly the sniff codes PHPCS reports on that line, with a reason
describing the boundary (anonymous core comment form, loopback cookie
forwarding, documented unvalidated referer whose callers validate the
redirect), or removed where nothing is reported. No hook, signature,
option or REST contract changes.
diff --git a/plugins/woocommerce/changelog/fix-woo6-115-public-interaction-suppressions b/plugins/woocommerce/changelog/fix-woo6-115-public-interaction-suppressions
new file mode 100644
index 00000000000..3013926e02b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-115-public-interaction-suppressions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Product review ratings posted as a fraction below one, such as 0.5, are no longer stored as a rating of 0.
diff --git a/plugins/woocommerce/includes/class-wc-background-emailer.php b/plugins/woocommerce/includes/class-wc-background-emailer.php
index 98aeb4e67bf..6557b56fafa 100644
--- a/plugins/woocommerce/includes/class-wc-background-emailer.php
+++ b/plugins/woocommerce/includes/class-wc-background-emailer.php
@@ -119,7 +119,7 @@ class WC_Background_Emailer extends WC_Background_Process {
// Pass cookies through with the request so nonces function.
$cookies = array();
- foreach ( $_COOKIE as $name => $value ) { // WPCS: input var ok.
+ foreach ( $_COOKIE as $name => $value ) {
if ( 'PHPSESSID' === $name ) {
continue;
}
diff --git a/plugins/woocommerce/includes/class-wc-comments.php b/plugins/woocommerce/includes/class-wc-comments.php
index 16be6d325b9..78153d14b02 100644
--- a/plugins/woocommerce/includes/class-wc-comments.php
+++ b/plugins/woocommerce/includes/class-wc-comments.php
@@ -224,7 +224,7 @@ class WC_Comments {
*/
public static function check_comment_rating( $comment_data ) {
// If posting a comment (not trackback etc) and not logged in.
- if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok.
+ if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Anonymous product reviews are submitted through the core comment form, which carries no WooCommerce nonce; the post ID is read through absint() and the rating is only tested for emptiness.
wp_die( esc_html__( 'Please rate the product.', 'woocommerce' ) );
exit;
}
@@ -237,13 +237,25 @@ class WC_Comments {
* @param int $comment_id Comment ID.
*/
public static function add_comment_rating( $comment_id ) {
- if ( isset( $_POST['rating'], $_POST['comment_post_ID'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
- if ( ! $_POST['rating'] || $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok, CSRF ok, sanitization ok.
+ if ( isset( $_POST['rating'], $_POST['comment_post_ID'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Anonymous product reviews are submitted through the core comment form, which carries no WooCommerce nonce; the post ID is read through absint().
+ $raw_rating = is_scalar( $_POST['rating'] ) ? wp_unslash( $_POST['rating'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Anonymous product reviews are submitted through the core comment form, which carries no WooCommerce nonce; the value is range checked and cast to an integer below.
+
+ // Stored ratings are whole numbers from 1 to 5. Numeric input is range checked before it
+ // is truncated, so 0.5 and 5.9 are rejected rather than stored as 0 and 5, while 4.7
+ // still stores 4 as before.
+ if ( is_numeric( $raw_rating ) && ( $raw_rating < 1 || $raw_rating > 5 ) ) {
+ return;
+ }
+
+ $rating = intval( $raw_rating );
+
+ if ( $rating < 1 || $rating > 5 ) {
return;
}
- add_comment_meta( $comment_id, 'rating', intval( $_POST['rating'] ), true ); // WPCS: input var ok, CSRF ok.
- $post_id = isset( $_POST['comment_post_ID'] ) ? absint( $_POST['comment_post_ID'] ) : 0; // WPCS: input var ok, CSRF ok.
+ add_comment_meta( $comment_id, 'rating', $rating, true );
+
+ $post_id = isset( $_POST['comment_post_ID'] ) ? absint( $_POST['comment_post_ID'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Anonymous product reviews are submitted through the core comment form, which carries no WooCommerce nonce; the post ID is read through absint().
if ( $post_id ) {
self::clear_transients( $post_id );
}
@@ -642,7 +654,7 @@ class WC_Comments {
* @return array
*/
public static function update_comment_type( $comment_data ) {
- if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok.
+ if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Anonymous product reviews are submitted through the core comment form, which carries no WooCommerce nonce; the post ID is read through absint().
$comment_data['comment_type'] = 'review';
}
diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php
index 1701aa083ee..855ef8d3533 100644
--- a/plugins/woocommerce/includes/wc-cart-functions.php
+++ b/plugins/woocommerce/includes/wc-cart-functions.php
@@ -55,10 +55,10 @@ function wc_get_raw_referer() {
return wp_get_raw_referer();
}
- if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok.
- return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
- } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok.
- return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
+ if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only fallback that only reports where the request came from; no state changes here.
+ return wp_unslash( $_REQUEST['_wp_http_referer'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This function is documented to return the referer unvalidated; every caller passes the result through wp_validate_redirect().
+ } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) {
+ return wp_unslash( $_SERVER['HTTP_REFERER'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This function is documented to return the referer unvalidated; every caller passes the result through wp_validate_redirect().
}
return false;
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-comments-test.php b/plugins/woocommerce/tests/php/includes/class-wc-comments-test.php
index 913d126d795..fa667b846c6 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-comments-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-comments-test.php
@@ -225,4 +225,74 @@ class WC_Comments_Tests extends \WC_Unit_Test_Case {
wp_delete_comment( $comment_id, true );
wp_delete_post( $post_id, true );
}
+
+ /**
+ * @testdox add_comment_rating() stores a rating only for whole numbers from 1 to 5.
+ *
+ * @testWith ["1", 1]
+ * ["3", 3]
+ * ["5", 5]
+ * ["05", 5]
+ * ["5.0", 5]
+ * ["4.7", 4]
+ * ["3abc", 3]
+ * ["", null]
+ * ["0", null]
+ * ["6", null]
+ * ["-1", null]
+ * ["-0.5", null]
+ * ["0.5", null]
+ * ["0.9", null]
+ * ["5.9", null]
+ * ["abc", null]
+ *
+ * @param string $posted_rating Raw value posted in the rating field.
+ * @param int|null $expected_rating Rating expected in comment meta, or null when nothing should be stored.
+ */
+ public function test_add_comment_rating_stores_only_whole_ratings_in_range( string $posted_rating, ?int $expected_rating ): void {
+ $this->assert_posted_rating_is_stored_as( $posted_rating, $expected_rating );
+ }
+
+ /**
+ * @testdox add_comment_rating() stores nothing when the rating field is posted as an array.
+ */
+ public function test_add_comment_rating_ignores_an_array_rating(): void {
+ $this->assert_posted_rating_is_stored_as( array( '5' ), null );
+ }
+
+ /**
+ * Post a rating for a fresh product review and assert what ends up in comment meta.
+ *
+ * @param mixed $posted_rating Raw value to place in $_POST['rating'].
+ * @param int|null $expected_rating Rating expected in comment meta, or null when nothing should be stored.
+ */
+ private function assert_posted_rating_is_stored_as( $posted_rating, ?int $expected_rating ): void {
+ $product = WC_Helper_Product::create_simple_product();
+ $comment_id = wp_insert_comment(
+ array(
+ 'comment_post_ID' => $product->get_id(),
+ 'comment_type' => 'review',
+ 'comment_approved' => '1',
+ )
+ );
+
+ $_POST['comment_post_ID'] = (string) $product->get_id();
+ $_POST['rating'] = $posted_rating;
+
+ try {
+ WC_Comments::add_comment_rating( $comment_id );
+
+ $stored = get_comment_meta( $comment_id, 'rating', true );
+
+ if ( null === $expected_rating ) {
+ $this->assertSame( '', $stored, 'No rating should have been stored.' );
+ } else {
+ $this->assertSame( $expected_rating, (int) $stored, 'The stored rating does not match.' );
+ }
+ } finally {
+ unset( $_POST['comment_post_ID'], $_POST['rating'] );
+ wp_delete_comment( $comment_id, true );
+ $product->delete( true );
+ }
+ }
}