Commit dbdd1112840 for woocommerce
commit dbdd11128403c1fb851cd9a7824d8e691df45906
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Mon Aug 31 07:14:43 2026 -0400
Fix mis-scoped PHPCS output suppressions in dashboard, reviews widget, and loop templates (#68160)
diff --git a/plugins/woocommerce/changelog/fix-woo6-120-mis-scoped-output-suppressions b/plugins/woocommerce/changelog/fix-woo6-120-mis-scoped-output-suppressions
new file mode 100644
index 00000000000..aa53ef2cc90
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woo6-120-mis-scoped-output-suppressions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Escape product titles in the dashboard top-seller report and correct output annotations.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
index dc325713496..d4d1793a01d 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-dashboard.php
@@ -224,8 +224,8 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
printf(
/* translators: %s: net sales */
esc_html__( 'Net sales this month %s', 'woocommerce' ),
- '<strong>' . wc_price( $report_data->net_sales ) . '</strong>'
- ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
+ '<strong>' . wc_price( $report_data->net_sales ) . '</strong>' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wc_price() returns filterable price markup for the dashboard.
+ );
?>
</a>
</li>
@@ -244,9 +244,9 @@ if ( ! class_exists( 'WC_Admin_Dashboard', false ) ) :
printf(
/* translators: 1: top seller product title 2: top seller quantity sold */
esc_html( _n( 'Top seller this month %1$s (%2$d sale)', 'Top seller this month %1$s (%2$d sales)', $top_seller->qty, 'woocommerce' ) ),
- '<strong>' . get_the_title( $top_seller->product_id ) . '</strong>',
- $top_seller->qty
- ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
+ '<strong>' . esc_html( get_the_title( $top_seller->product_id ) ) . '</strong>',
+ (int) $top_seller->qty
+ );
?>
</a>
</li>
diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php b/plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php
index eb32a5da77e..20b30b974fb 100644
--- a/plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php
+++ b/plugins/woocommerce/includes/widgets/class-wc-widget-recent-reviews.php
@@ -57,7 +57,7 @@ class WC_Widget_Recent_Reviews extends WC_Widget {
ob_start();
$number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std'];
- $comments = get_comments(
+ $comments = get_comments( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The global is the legacy data contract consumed by review templates.
array(
'number' => $number,
'status' => 'approve',
@@ -66,14 +66,14 @@ class WC_Widget_Recent_Reviews extends WC_Widget {
'parent' => 0,
'update_comment_post_cache' => true,
)
- ); // WPCS: override ok.
+ );
if ( $comments ) {
$this->widget_start( $args, $instance );
echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_review_list', '<ul class="product_list_widget">' ) );
- foreach ( (array) $comments as $comment ) {
+ foreach ( (array) $comments as $comment ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Each review template consumes the current comment through this WordPress global.
wc_get_template(
'content-widget-reviews.php',
array(
diff --git a/plugins/woocommerce/templates/loop/add-to-cart.php b/plugins/woocommerce/templates/loop/add-to-cart.php
index 934ba5db90d..b06497c6fbb 100644
--- a/plugins/woocommerce/templates/loop/add-to-cart.php
+++ b/plugins/woocommerce/templates/loop/add-to-cart.php
@@ -23,8 +23,17 @@ global $product;
$aria_describedby = isset( $args['aria-describedby_text'] ) ? sprintf( 'aria-describedby="woocommerce_loop_add_to_cart_link_describedby_%s"', esc_attr( $product->get_id() ) ) : '';
-echo apply_filters(
- 'woocommerce_loop_add_to_cart_link', // WPCS: XSS ok.
+/**
+ * Filters the loop add-to-cart link HTML.
+ *
+ * @param string $link The add-to-cart link HTML.
+ * @param WC_Product $product The product object.
+ * @param array $args Arguments used to build the link.
+ *
+ * @since 2.0.0
+ */
+echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The public filter intentionally allows extensions to return link markup.
+ 'woocommerce_loop_add_to_cart_link',
sprintf(
'<a href="%s" %s data-quantity="%s" class="%s" %s>%s</a>',
esc_url( $product->add_to_cart_url() ),
diff --git a/plugins/woocommerce/templates/loop/pagination.php b/plugins/woocommerce/templates/loop/pagination.php
index ab909777c1f..06ada288a8b 100644
--- a/plugins/woocommerce/templates/loop/pagination.php
+++ b/plugins/woocommerce/templates/loop/pagination.php
@@ -30,10 +30,17 @@ if ( $total <= 1 ) {
?>
<nav class="woocommerce-pagination" aria-label="<?php esc_attr_e( 'Product Pagination', 'woocommerce' ); ?>">
<?php
- echo paginate_links(
+ echo paginate_links( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WordPress generates the pagination markup after applying the public arguments filter.
+ /**
+ * Filters the pagination arguments for the product loop.
+ *
+ * @param array $args Pagination arguments.
+ *
+ * @since 2.0.0
+ */
apply_filters(
'woocommerce_pagination_args',
- array( // WPCS: XSS ok.
+ array(
'base' => $base,
'format' => $format,
'add_args' => false,
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-test.php
index 22978797280..243adf5ef96 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-test.php
@@ -1,7 +1,11 @@
<?php
declare( strict_types = 1 );
+use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Tests for the WC_Admin_Dashboard class.
@@ -13,6 +17,24 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
* WC_Admin_Dashboard_Test
*/
class WC_Admin_Dashboard_Test extends WC_Unit_Test_Case {
+ use HPOSToggleTrait;
+
+ /**
+ * Ensure the HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ try {
+ self::setup_cot_tables();
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== $previous_hpos_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( $previous_hpos_state );
+ }
+ } finally {
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ }
+ }
/**
* The system under test.
@@ -368,6 +390,75 @@ class WC_Admin_Dashboard_Test extends WC_Unit_Test_Case {
$this->assertStringContainsString( 'On-hold <strong>0 orders</strong>', $html );
}
+ /**
+ * @testdox The top-seller title is escaped without removing dashboard markup under legacy and HPOS storage.
+ * @testWith [false]
+ * [true]
+ *
+ * @param bool $hpos_enabled Whether HPOS is enabled.
+ */
+ public function test_status_widget_escapes_top_seller_title_for_each_order_storage( bool $hpos_enabled ): void {
+ $previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+
+ $product = null;
+ $order = null;
+
+ $reports_filter = static function ( array $reports ): array {
+ $reports['get_sales_sparkline'] = static fn() => array(
+ 'total' => 0,
+ 'data' => array(),
+ );
+
+ return $reports;
+ };
+
+ try {
+ $this->toggle_cot_authoritative( $hpos_enabled );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $order = wc_create_order();
+ $order->add_product( $product, 2 );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->save();
+
+ $title_filter = static function ( string $title, int $post_id ) use ( $product ) {
+ return $product->get_id() === $post_id ? 'Unsafe <script>alert("dashboard")</script> & title' : $title;
+ };
+
+ add_filter( 'woocommerce_admin_disabled', '__return_true' );
+ add_filter( 'woocommerce_dashboard_status_widget_reports', $reports_filter );
+ add_filter( 'the_title', $title_filter, 10, 2 );
+
+ ob_start();
+ $this->sut->status_widget_content();
+ $html = ob_get_clean();
+
+ $this->assertStringContainsString( '<strong>Unsafe <script>alert("dashboard")</script> & title</strong> (2 sales)', $html );
+ $this->assertStringNotContainsString( '<script>', $html );
+ } finally {
+ remove_filter( 'woocommerce_admin_disabled', '__return_true' );
+ remove_filter( 'woocommerce_dashboard_status_widget_reports', $reports_filter );
+ if ( isset( $title_filter ) ) {
+ remove_filter( 'the_title', $title_filter, 10 );
+ }
+ if ( $order instanceof WC_Order ) {
+ $order->delete( true );
+ }
+ if ( $product instanceof WC_Product ) {
+ $product->delete( true );
+ }
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== $previous_hpos_state ) {
+ $this->toggle_cot_authoritative( $previous_hpos_state );
+ }
+ add_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ add_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ }
+ }
+
/**
* @testdox Widget does not show without proper capabilities.
*/