Reviews
*/
namespace Automattic\WooCommerce\Internal\Admin\ProductReviews;
use WC_Product;
use WP_Comment;
use WP_Comments_List_Table;
use WP_List_Table;
use WP_Post;
/**
* Handles the Product Reviews page.
*/
class ReviewsListTable extends WP_List_Table {
/**
* Memoization flag to determine if the current user can edit the current review.
*
* @var bool
*/
private $current_user_can_edit_review = false;
/**
* Memoization flag to determine if the current user can moderate reviews.
*
* @var bool
*/
private $current_user_can_moderate_reviews;
/**
* Current rating of reviews to display.
*
* @var int
*/
private $current_reviews_rating = 0;
/**
* Current product the reviews should be displayed for.
*
* @var WC_Product|null Product or null for all products.
*/
private $current_product_for_reviews;
/**
* Constructor.
*
* @param array|string $args Array or string of arguments.
*/
public function __construct( $args = [] ) {
parent::__construct(
wp_parse_args(
$args,
[
'plural' => 'product-reviews',
'singular' => 'product-review',
]
)
);
$this->current_user_can_moderate_reviews = current_user_can( Reviews::get_capability( 'moderate' ) );
}
/**
* Prepares reviews for display.
*
* @return void
*/
public function prepare_items() : void {
$this->set_review_status();
$this->set_review_type();
$this->current_reviews_rating = isset( $_REQUEST['review_rating'] ) ? absint( $_REQUEST['review_rating'] ) : 0;
$this->set_review_product();
$args = [
'number' => $this->get_per_page(),
'post_type' => 'product',
];
// Include the order & orderby arguments.
$args = wp_parse_args( $this->get_sort_arguments(), $args );
// Handle the review item types filter.
$args = wp_parse_args( $this->get_filter_type_arguments(), $args );
// Handle the reviews rating filter.
$args = wp_parse_args( $this->get_filter_rating_arguments(), $args );
// Handle the review product filter.
$args = wp_parse_args( $this->get_filter_product_arguments(), $args );
// Include the review status arguments.
$args = wp_parse_args( $this->get_status_arguments(), $args );
// Include the search argument.
$args = wp_parse_args( $this->get_search_arguments(), $args );
// Include the offset argument.
$args = wp_parse_args( $this->get_offset_arguments(), $args );
/**
* Provides an opportunity to alter the comment query arguments used within
* the product reviews admin list table.
*
* @since 7.0.0
*
* @param array $args Comment query args.
*/
$args = (array) apply_filters( 'woocommerce_product_reviews_list_table_prepare_items_args', $args );
$comments = get_comments( $args );
update_comment_cache( $comments );
$this->items = $comments;
$this->set_pagination_args(
[
'total_items' => get_comments( $this->get_total_comments_arguments( $args ) ),
'per_page' => $this->get_per_page(),
]
);
}
/**
* Returns the number of items to show per page.
*
* @return int Customized per-page value if available, or 20 as the default.
*/
protected function get_per_page() : int {
return $this->get_items_per_page( 'edit_comments_per_page' );
}
/**
* Sets the product to filter reviews by.
*
* @return void
*/
protected function set_review_product() : void {
$product_id = isset( $_REQUEST['product_id'] ) ? absint( $_REQUEST['product_id'] ) : null;
$product = $product_id ? wc_get_product( $product_id ) : null;
if ( $product instanceof WC_Product ) {
$this->current_product_for_reviews = $product;
}
}
/**
* Sets the `$comment_status` global based on the current request.
*
* @global string $comment_status
*
* @return void
*/
protected function set_review_status() : void {
global $comment_status;
$comment_status = sanitize_text_field( wp_unslash( $_REQUEST['comment_status'] ?? 'all' ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
if ( ! in_array( $comment_status, [ 'all', 'moderated', 'approved', 'spam', 'trash' ], true ) ) {
$comment_status = 'all'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
}
/**
* Sets the `$comment_type` global based on the current request.
*
* @global string $comment_type
*
* @return void
*/
protected function set_review_type() : void {
global $comment_type;
$review_type = sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ?? 'all' ) );
if ( 'all' !== $review_type && ! empty( $review_type ) ) {
$comment_type = $review_type; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
}
/**
* Builds the `orderby` and `order` arguments based on the current request.
*
* @return array
*/
protected function get_sort_arguments() : array {
$orderby = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ?? '' ) );
$order = sanitize_text_field( wp_unslash( $_REQUEST['order'] ?? '' ) );
$args = [];
if ( ! in_array( $orderby, $this->get_sortable_columns(), true ) ) {
$orderby = 'comment_date_gmt';
}
// If ordering by "rating", then we need to adjust to sort by meta value.
if ( 'rating' === $orderby ) {
$orderby = 'meta_value_num';
$args['meta_key'] = 'rating';
}
if ( ! in_array( strtolower( $order ), [ 'asc', 'desc' ], true ) ) {
$order = 'desc';
}
return wp_parse_args(
[
'orderby' => $orderby,
'order' => strtolower( $order ),
],
$args
);
}
/**
* Builds the `type` argument based on the current request.
*
* @return array
*/
protected function get_filter_type_arguments() : array {
$args = [];
$item_type = isset( $_REQUEST['review_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['review_type'] ) ) : 'all';
if ( 'all' === $item_type ) {
return $args;
}
$args['type'] = $item_type;
return $args;
}
/**
* Builds the `meta_query` arguments based on the current request.
*
* @return array
*/
protected function get_filter_rating_arguments() : array {
$args = [];
if ( empty( $this->current_reviews_rating ) ) {
return $args;
}
$args['meta_query'] = [
[
'key' => 'rating',
'value' => (int) $this->current_reviews_rating,
'compare' => '=',
'type' => 'NUMERIC',
],
];
return $args;
}
/**
* Gets the `post_id` argument based on the current request.
*
* @return array
*/
public function get_filter_product_arguments() : array {
$args = [];
if ( $this->current_product_for_reviews instanceof WC_Product ) {
$args['post_id'] = $this->current_product_for_reviews->get_id();
}
return $args;
}
/**
* Gets the `status` argument based on the current request.
*
* @return array
*/
protected function get_status_arguments() : array {
$args = [];
global $comment_status;
if ( ! empty( $comment_status ) && 'all' !== $comment_status && array_key_exists( $comment_status, $this->get_status_filters() ) ) {
$args['status'] = $this->convert_status_to_query_value( $comment_status );
}
return $args;
}
/**
* Gets the `search` argument based on the current request.
*
* @return array
*/
protected function get_search_arguments() : array {
$args = [];
if ( ! empty( $_REQUEST['s'] ) ) {
$args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) );
}
return $args;
}
/**
* Returns the `offset` argument based on the current request.
*
* @return array
*/
protected function get_offset_arguments() : array {
$args = [];
if ( isset( $_REQUEST['start'] ) ) {
$args['offset'] = absint( wp_unslash( $_REQUEST['start'] ) );
} else {
$args['offset'] = ( $this->get_pagenum() - 1 ) * $this->get_per_page();
}
return $args;
}
/**
* Returns the arguments used to count the total number of comments.
*
* @param array $default_query_args Query args for the main request.
* @return array
*/
protected function get_total_comments_arguments( array $default_query_args ) : array {
return wp_parse_args(
[
'count' => true,
'offset' => 0,
'number' => 0,
],
$default_query_args
);
}
/**
* Displays the product reviews HTML table.
*
* Reimplements {@see WP_Comment_::display()} but we change the ID to match the one output by {@see WP_Comments_List_Table::display()}.
* This will automatically handle additional CSS for consistency with the comments page.
*
* @return void
*/
public function display() : void {
$this->display_tablenav( 'top' );
$this->screen->render_screen_reader_content( 'heading_list' );
?>
print_column_headers(); ?>
print_column_headers( false ); ?>
display_tablenav( 'bottom' );
}
/**
* Render a single row HTML.
*
* @global WP_Post $post
* @global WP_Comment $comment
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
public function single_row( $item ) : void {
global $post, $comment;
// Overrides the comment global for properly rendering rows.
$comment = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$the_comment_class = (string) wp_get_comment_status( $comment->comment_ID );
$the_comment_class = implode( ' ', get_comment_class( $the_comment_class, $comment->comment_ID, $comment->comment_post_ID ) );
// Sets the post for the product in context.
$post = get_post( $comment->comment_post_ID ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$this->current_user_can_edit_review = current_user_can( 'edit_comment', $comment->comment_ID );
?>
current_user_can_edit_review ) {
return '';
}
$review_status = wp_get_comment_status( $item );
$url = add_query_arg(
[
'c' => urlencode( $item->comment_ID ),
],
admin_url( 'comment.php' )
);
$approve_url = wp_nonce_url( add_query_arg( 'action', 'approvecomment', $url ), "approve-comment_$item->comment_ID" );
$unapprove_url = wp_nonce_url( add_query_arg( 'action', 'unapprovecomment', $url ), "approve-comment_$item->comment_ID" );
$spam_url = wp_nonce_url( add_query_arg( 'action', 'spamcomment', $url ), "delete-comment_$item->comment_ID" );
$unspam_url = wp_nonce_url( add_query_arg( 'action', 'unspamcomment', $url ), "delete-comment_$item->comment_ID" );
$trash_url = wp_nonce_url( add_query_arg( 'action', 'trashcomment', $url ), "delete-comment_$item->comment_ID" );
$untrash_url = wp_nonce_url( add_query_arg( 'action', 'untrashcomment', $url ), "delete-comment_$item->comment_ID" );
$delete_url = wp_nonce_url( add_query_arg( 'action', 'deletecomment', $url ), "delete-comment_$item->comment_ID" );
$actions = [
'approve' => '',
'unapprove' => '',
'reply' => '',
'quickedit' => '',
'edit' => '',
'spam' => '',
'unspam' => '',
'trash' => '',
'untrash' => '',
'delete' => '',
];
if ( $comment_status && 'all' !== $comment_status ) {
if ( 'approved' === $review_status ) {
$actions['unapprove'] = sprintf(
'%s',
esc_url( $unapprove_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&new=unapproved" ),
esc_attr__( 'Unapprove this review', 'woocommerce' ),
esc_html__( 'Unapprove', 'woocommerce' )
);
} elseif ( 'unapproved' === $review_status ) {
$actions['approve'] = sprintf(
'%s',
esc_url( $approve_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:e7e7d3:action=dim-comment&new=approved" ),
esc_attr__( 'Approve this review', 'woocommerce' ),
esc_html__( 'Approve', 'woocommerce' )
);
}
} else {
$actions['approve'] = sprintf(
'%s',
esc_url( $approve_url ),
esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved" ),
esc_attr__( 'Approve this review', 'woocommerce' ),
esc_html__( 'Approve', 'woocommerce' )
);
$actions['unapprove'] = sprintf(
'%s',
esc_url( $unapprove_url ),
esc_attr( "dim:the-comment-list:comment-{$item->comment_ID}:unapproved:e7e7d3:e7e7d3:new=unapproved" ),
esc_attr__( 'Unapprove this review', 'woocommerce' ),
esc_html__( 'Unapprove', 'woocommerce' )
);
}
if ( 'spam' !== $review_status ) {
$actions['spam'] = sprintf(
'%s',
esc_url( $spam_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::spam=1" ),
esc_attr__( 'Mark this review as spam', 'woocommerce' ),
/* translators: "Mark as spam" link. */
esc_html_x( 'Spam', 'verb', 'woocommerce' )
);
} else {
$actions['unspam'] = sprintf(
'%s',
esc_url( $unspam_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:unspam=1" ),
esc_attr__( 'Restore this review from the spam', 'woocommerce' ),
esc_html_x( 'Not Spam', 'review', 'woocommerce' )
);
}
if ( 'trash' === $review_status ) {
$actions['untrash'] = sprintf(
'%s',
esc_url( $untrash_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}:66cc66:untrash=1" ),
esc_attr__( 'Restore this review from the Trash', 'woocommerce' ),
esc_html__( 'Restore', 'woocommerce' )
);
}
if ( 'spam' === $review_status || 'trash' === $review_status || ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = sprintf(
'%s',
esc_url( $delete_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::delete=1" ),
esc_attr__( 'Delete this review permanently', 'woocommerce' ),
esc_html__( 'Delete Permanently', 'woocommerce' )
);
} else {
$actions['trash'] = sprintf(
'%s',
esc_url( $trash_url ),
esc_attr( "delete:the-comment-list:comment-{$item->comment_ID}::trash=1" ),
esc_attr__( 'Move this review to the Trash', 'woocommerce' ),
esc_html_x( 'Trash', 'verb', 'woocommerce' )
);
}
if ( 'spam' !== $review_status && 'trash' !== $review_status ) {
$actions['edit'] = sprintf(
'%s',
esc_url(
add_query_arg(
[
'action' => 'editcomment',
'c' => urlencode( $item->comment_ID ),
],
admin_url( 'comment.php' )
)
),
esc_attr__( 'Edit this review', 'woocommerce' ),
esc_html__( 'Edit', 'woocommerce' )
);
$format = '';
$actions['quickedit'] = sprintf(
$format,
esc_attr( $item->comment_ID ),
esc_attr( $item->comment_post_ID ),
'edit',
'vim-q comment-inline',
esc_attr__( 'Quick edit this review inline', 'woocommerce' ),
esc_html__( 'Quick Edit', 'woocommerce' )
);
$actions['reply'] = sprintf(
$format,
esc_attr( $item->comment_ID ),
esc_attr( $item->comment_post_ID ),
'replyto',
'vim-r comment-inline',
esc_attr__( 'Reply to this review', 'woocommerce' ),
esc_html__( 'Reply', 'woocommerce' )
);
}
$always_visible = 'excerpt' === get_user_setting( 'posts_list_mode', 'list' );
$output = '';
$i = 0;
foreach ( array_filter( $actions ) as $action => $link ) {
++$i;
if ( ( ( 'approve' === $action || 'unapprove' === $action ) && 2 === $i ) || 1 === $i ) {
$sep = '';
} else {
$sep = ' | ';
}
if ( ( 'reply' === $action || 'quickedit' === $action ) && ! wp_doing_ajax() ) {
$action .= ' hide-if-no-js';
} elseif ( ( 'untrash' === $action && 'trash' === $review_status ) || ( 'unspam' === $action && 'spam' === $review_status ) ) {
if ( '1' === get_comment_meta( $item->comment_ID, '_wp_trash_meta_status', true ) ) {
$action .= ' approve';
} else {
$action .= ' unapprove';
}
}
$output .= "$sep$link";
}
$output .= '
';
$output .= '';
return $output;
}
/**
* Gets the columns for the table.
*
* @return array Table columns and their headings.
*/
public function get_columns() : array {
$columns = [
'cb' => '',
'type' => _x( 'Type', 'review type', 'woocommerce' ),
'author' => __( 'Author', 'woocommerce' ),
'rating' => __( 'Rating', 'woocommerce' ),
'comment' => _x( 'Review', 'column name', 'woocommerce' ),
'response' => __( 'Product', 'woocommerce' ),
'date' => _x( 'Submitted on', 'column name', 'woocommerce' ),
];
/**
* Filters the table columns.
*
* @since 6.7.0
*
* @param array $columns
*/
return (array) apply_filters( 'woocommerce_product_reviews_table_columns', $columns );
}
/**
* Gets the name of the default primary column.
*
* @return string Name of the primary column.
*/
protected function get_primary_column_name() : string {
return 'comment';
}
/**
* Gets a list of sortable columns.
*
* Key is the column ID and value is which database column we perform the sorting on.
* The `rating` column uses a unique key instead, as that requires sorting by meta value.
*
* @return array
*/
protected function get_sortable_columns() : array {
return [
'author' => 'comment_author',
'response' => 'comment_post_ID',
'date' => 'comment_date_gmt',
'type' => 'comment_type',
'rating' => 'rating',
];
}
/**
* Returns a list of available bulk actions.
*
* @global string $comment_status
*
* @return array
*/
protected function get_bulk_actions() : array {
global $comment_status;
$actions = [];
if ( in_array( $comment_status, [ 'all', 'approved' ], true ) ) {
$actions['unapprove'] = __( 'Unapprove', 'woocommerce' );
}
if ( in_array( $comment_status, [ 'all', 'moderated' ], true ) ) {
$actions['approve'] = __( 'Approve', 'woocommerce' );
}
if ( in_array( $comment_status, [ 'all', 'moderated', 'approved', 'trash' ], true ) ) {
$actions['spam'] = _x( 'Mark as spam', 'review', 'woocommerce' );
}
if ( 'trash' === $comment_status ) {
$actions['untrash'] = __( 'Restore', 'woocommerce' );
} elseif ( 'spam' === $comment_status ) {
$actions['unspam'] = _x( 'Not spam', 'review', 'woocommerce' );
}
if ( in_array( $comment_status, [ 'trash', 'spam' ], true ) || ! EMPTY_TRASH_DAYS ) {
$actions['delete'] = __( 'Delete permanently', 'woocommerce' );
} else {
$actions['trash'] = __( 'Move to Trash', 'woocommerce' );
}
return $actions;
}
/**
* Returns the current action select in bulk actions menu.
*
* This is overridden in order to support `delete_all` for use in {@see ReviewsListTable::process_bulk_action()}
*
* {@see WP_Comments_List_Table::current_action()} for reference.
*
* @return string|false
*/
public function current_action() {
if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) {
return 'delete_all';
}
return parent::current_action();
}
/**
* Processes the bulk actions.
*
* @return void
*/
public function process_bulk_action() : void {
if ( ! $this->current_user_can_moderate_reviews ) {
return;
}
if ( $this->current_action() ) {
check_admin_referer( 'bulk-product-reviews' );
$query_string = remove_query_arg( [ 'page', '_wpnonce' ], wp_unslash( ( $_SERVER['QUERY_STRING'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Replace current nonce with bulk-comments nonce.
$comments_nonce = wp_create_nonce( 'bulk-comments' );
$query_string = add_query_arg( '_wpnonce', $comments_nonce, $query_string );
// Redirect to edit-comments.php, which will handle processing the action for us.
wp_safe_redirect( esc_url_raw( admin_url( 'edit-comments.php?' . $query_string ) ) );
exit;
} elseif ( ! empty( $_GET['_wp_http_referer'] ) ) {
wp_safe_redirect( remove_query_arg( [ '_wp_http_referer', '_wpnonce' ] ) );
exit;
}
}
/**
* Returns an array of supported statuses and their labels.
*
* @return array
*/
protected function get_status_filters() : array {
return [
/* translators: %s: Number of reviews. */
'all' => _nx_noop(
'All (%s)',
'All (%s)',
'product reviews',
'woocommerce'
),
/* translators: %s: Number of reviews. */
'moderated' => _nx_noop(
'Pending (%s)',
'Pending (%s)',
'product reviews',
'woocommerce'
),
/* translators: %s: Number of reviews. */
'approved' => _nx_noop(
'Approved (%s)',
'Approved (%s)',
'product reviews',
'woocommerce'
),
/* translators: %s: Number of reviews. */
'spam' => _nx_noop(
'Spam (%s)',
'Spam (%s)',
'product reviews',
'woocommerce'
),
/* translators: %s: Number of reviews. */
'trash' => _nx_noop(
'Trash (%s)',
'Trash (%s)',
'product reviews',
'woocommerce'
),
];
}
/**
* Returns the available status filters.
*
* @see WP_Comments_List_Table::get_views() for consistency.
*
* @global int $post_id
* @global string $comment_status
* @global string $comment_type
*
* @return array An associative array of fully-formed comment status links. Includes 'All', 'Pending', 'Approved', 'Spam', and 'Trash'.
*/
protected function get_views() : array {
global $post_id, $comment_status, $comment_type;
$status_links = [];
$status_labels = $this->get_status_filters();
if ( ! EMPTY_TRASH_DAYS ) {
unset( $status_labels['trash'] );
}
$link = $this->get_view_url( (string) $comment_type, (int) $post_id );
foreach ( $status_labels as $status => $label ) {
$current_link_attributes = '';
if ( $status === $comment_status ) {
$current_link_attributes = ' class="current" aria-current="page"';
}
$link = add_query_arg( 'comment_status', urlencode( $status ), $link );
$number_reviews_for_status = $this->get_review_count( $status, (int) $post_id );
$count_html = sprintf(
'%s',
( 'moderated' === $status ) ? 'pending' : $status,
number_format_i18n( $number_reviews_for_status )
);
$status_links[ $status ] = '' . sprintf( translate_nooped_plural( $label, $number_reviews_for_status ), $count_html ) . '';
}
return $status_links;
}
/**
* Gets the base URL for a view, excluding the status (that should be appended).
*
* @param string $comment_type Comment type filter.
* @param int $post_id Current post ID.
* @return string
*/
protected function get_view_url( string $comment_type, int $post_id ) : string {
$link = Reviews::get_reviews_page_url();
if ( ! empty( $comment_type ) && 'all' !== $comment_type ) {
$link = add_query_arg( 'comment_type', urlencode( $comment_type ), $link );
}
if ( ! empty( $post_id ) ) {
$link = add_query_arg( 'p', absint( $post_id ), $link );
}
return $link;
}
/**
* Gets the number of reviews (including review replies) for a given status.
*
* @param string $status Status key from {@see ReviewsListTable::get_status_filters()}.
* @param int $product_id ID of the product if we're filtering by product in this request. Otherwise, `0` for no product filters.
* @return int
*/
protected function get_review_count( string $status, int $product_id ) : int {
return (int) get_comments(
[
'type__in' => [ 'review', 'comment' ],
'status' => $this->convert_status_to_query_value( $status ),
'post_type' => 'product',
'post_id' => $product_id,
'count' => true,
]
);
}
/**
* Converts a status key into its equivalent `comment_approved` database column value.
*
* @param string $status Status key from {@see ReviewsListTable::get_status_filters()}.
* @return string
*/
protected function convert_status_to_query_value( string $status ) : string {
// These keys exactly match the database column.
if ( in_array( $status, [ 'spam', 'trash' ], true ) ) {
return $status;
}
switch ( $status ) {
case 'moderated':
return '0';
case 'approved':
return '1';
default:
return 'all';
}
}
/**
* Outputs the text to display when there are no reviews to display.
*
* @see WP_List_Table::no_items()
*
* @global string $comment_status
*
* @return void
*/
public function no_items() : void {
global $comment_status;
if ( 'moderated' === $comment_status ) {
esc_html_e( 'No reviews awaiting moderation.', 'woocommerce' );
} else {
esc_html_e( 'No reviews found.', 'woocommerce' );
}
}
/**
* Renders the checkbox column.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_cb( $item ) : void {
ob_start();
if ( $this->current_user_can_edit_review ) {
?>
filter_column_output( 'cb', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders the review column.
*
* @see WP_Comments_List_Table::column_comment() for consistency.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_comment( $item ) : void {
$in_reply_to = $this->get_in_reply_to_review_text( $item );
ob_start();
if ( $in_reply_to ) {
echo $in_reply_to . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
printf(
'%1$s%2$s%3$s',
''
);
if ( $this->current_user_can_edit_review ) {
?>
filter_column_output( 'comment', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Gets the in-reply-to-review text.
*
* @param WP_Comment|mixed $reply Reply to review.
* @return string
*/
private function get_in_reply_to_review_text( $reply ) : string {
$review = $reply->comment_parent ? get_comment( $reply->comment_parent ) : null;
if ( ! $review ) {
return '';
}
$parent_review_link = get_comment_link( $review );
$review_author_name = get_comment_author( $review );
return sprintf(
/* translators: %s: Parent review link with review author name. */
ent2ncr( __( 'In reply to %s.', 'woocommerce' ) ),
'' . esc_html( $review_author_name ) . ''
);
}
/**
* Renders the author column.
*
* @see WP_Comments_List_Table::column_author() for consistency.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_author( $item ) : void {
global $comment_status;
$author_url = $this->get_item_author_url();
$author_url_display = $this->get_item_author_url_for_display( $author_url );
if ( get_option( 'show_avatars' ) ) {
$author_avatar = get_avatar( $item, 32, 'mystery' );
} else {
$author_avatar = '';
}
ob_start();
echo '' . $author_avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
comment_author();
echo '
';
if ( ! empty( $author_url ) ) :
?>
current_user_can_edit_review ) :
if ( ! empty( $item->comment_author_email ) && is_email( $item->comment_author_email ) ) :
?>
comment_author_email ); ?>
urlencode( get_comment_author_IP( $item->comment_ID ) ),
'page' => Reviews::MENU_SLUG,
'mode' => 'detail',
],
'admin.php'
);
if ( 'spam' === $comment_status ) :
$link = add_query_arg( [ 'comment_status' => 'spam' ], $link );
endif;
?>
comment_ID ); ?>
filter_column_output( 'author', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Gets the item author URL.
*
* @return string
*/
private function get_item_author_url() : string {
$author_url = get_comment_author_url();
$protocols = [ 'https://', 'http://' ];
if ( in_array( $author_url, $protocols ) ) {
$author_url = '';
}
return $author_url;
}
/**
* Gets the item author URL for display.
*
* @param string $author_url The review or reply author URL (raw).
* @return string
*/
private function get_item_author_url_for_display( $author_url ) : string {
$author_url_display = untrailingslashit( preg_replace( '|^http(s)?://(www\.)?|i', '', $author_url ) );
if ( strlen( $author_url_display ) > 50 ) {
$author_url_display = wp_html_excerpt( $author_url_display, 49, '…' );
}
return $author_url_display;
}
/**
* Renders the "submitted on" column.
*
* Note that the output is consistent with {@see WP_Comments_List_Table::column_date()}.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_date( $item ) : void {
$submitted = sprintf(
/* translators: 1 - Product review date, 2: Product review time. */
__( '%1$s at %2$s', 'woocommerce' ),
/* translators: Review date format. See https://www.php.net/manual/datetime.format.php */
get_comment_date( __( 'Y/m/d', 'woocommerce' ), $item ),
/* translators: Review time format. See https://www.php.net/manual/datetime.format.php */
get_comment_date( __( 'g:i a', 'woocommerce' ), $item )
);
ob_start();
?>
comment_post_ID ) ) :
printf(
'
%2$s',
esc_url( get_comment_link( $item ) ),
esc_html( $submitted )
);
else :
echo esc_html( $submitted );
endif;
?>
filter_column_output( 'date', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders the product column.
*
* @see WP_Comments_List_Table::column_response() for consistency.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_response( $item ) : void {
$product_post = get_post();
ob_start();
if ( $product_post ) :
?>
ID ) ) :
$post_link = "';
else :
$post_link = esc_html( get_the_title( $product_post->ID ) );
endif;
echo $post_link; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$post_type_object = get_post_type_object( $product_post->post_type );
?>
comments_bubble( $product_post->ID, get_pending_comments_num( $product_post->ID ) ); ?>
filter_column_output( 'response', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders the type column.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_type( $item ) : void {
$type = 'review' === $item->comment_type
? '☆ ' . __( 'Review', 'woocommerce' )
: __( 'Reply', 'woocommerce' );
echo $this->filter_column_output( 'type', esc_html( $type ), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders the rating column.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @return void
*/
protected function column_rating( $item ) : void {
$rating = get_comment_meta( $item->comment_ID, 'rating', true );
ob_start();
if ( ! empty( $rating ) && is_numeric( $rating ) ) {
$rating = (int) $rating;
$accessibility_label = sprintf(
/* translators: 1: number representing a rating */
__( '%1$d out of 5', 'woocommerce' ),
$rating
);
$stars = str_repeat( '★', $rating );
$stars .= str_repeat( '☆', 5 - $rating );
?>
filter_column_output( 'rating', ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Renders any custom columns.
*
* @param WP_Comment|mixed $item Review or reply being rendered.
* @param string|mixed $column_name Name of the column being rendered.
* @return void
*/
protected function column_default( $item, $column_name ) : void {
ob_start();
/**
* Fires when the default column output is displayed for a single row.
*
* This action can be used to render custom columns that have been added.
*
* @since 6.7.0
*
* @param WP_Comment $item The review or reply being rendered.
*/
do_action( 'woocommerce_product_reviews_table_column_' . $column_name, $item );
echo $this->filter_column_output( $column_name, ob_get_clean(), $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Runs a filter hook for a given column content.
*
* @param string|mixed $column_name The column being output.
* @param string|mixed $output The output content (may include HTML).
* @param WP_Comment|mixed $item The review or reply being rendered.
* @return string
*/
protected function filter_column_output( $column_name, $output, $item ) : string {
/**
* Filters the output of a column.
*
* @since 6.7.0
*
* @param string $output The column output.
* @param WP_Comment $item The product review being rendered.
*/
return (string) apply_filters( 'woocommerce_product_reviews_table_column_' . $column_name . '_content', $output, $item );
}
/**
* Renders the extra controls to be displayed between bulk actions and pagination.
*
* @global string $comment_status
* @global string $comment_type
*
* @param string|mixed $which Position (top or bottom).
* @return void
*/
protected function extra_tablenav( $which ) : void {
global $comment_status, $comment_type;
echo '';
if ( 'top' === $which ) {
ob_start();
echo '';
$this->review_type_dropdown( $comment_type );
$this->review_rating_dropdown( $this->current_reviews_rating );
$this->product_search( $this->current_product_for_reviews );
echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
submit_button( __( 'Filter', 'woocommerce' ), '', 'filter_action', false, [ 'id' => 'post-query-submit' ] );
}
if ( ( 'spam' === $comment_status || 'trash' === $comment_status ) && $this->has_items() && $this->current_user_can_moderate_reviews ) {
wp_nonce_field( 'bulk-destroy', '_destroy_nonce' );
$title = 'spam' === $comment_status
? esc_attr__( 'Empty Spam', 'woocommerce' )
: esc_attr__( 'Empty Trash', 'woocommerce' );
submit_button( $title, 'apply', 'delete_all', false );
}
echo '
';
}
/**
* Displays a review type drop-down for filtering reviews in the Product Reviews list table.
*
* @see WP_Comments_List_Table::comment_type_dropdown() for consistency.
*
* @param string|mixed $current_type The current comment item type slug.
* @return void
*/
protected function review_type_dropdown( $current_type ) : void {
/**
* Sets the possible options used in the Product Reviews List Table's filter-by-review-type
* selector.
*
* @since 7.0.0
*
* @param array Map of possible review types.
*/
$item_types = apply_filters(
'woocommerce_product_reviews_list_table_item_types',
array(
'all' => __( 'All types', 'woocommerce' ),
'comment' => __( 'Replies', 'woocommerce' ),
'review' => __( 'Reviews', 'woocommerce' ),
)
);
?>
__( 'All ratings', 'woocommerce' ),
'1' => '★',
'2' => '★★',
'3' => '★★★',
'4' => '★★★★',
'5' => '★★★★★',
];
?>
—%s',
esc_html__( 'No reviews', 'woocommerce' )
);
} elseif ( $approved_review_count && 'trash' === get_post_status( $post_id ) ) {
// Don't link the comment bubble for a trashed product.
printf(
'%s',
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
);
} elseif ( $approved_review_count ) {
// Link the comment bubble to approved reviews.
printf(
'%s',
esc_url(
add_query_arg(
[
'product_id' => urlencode( $post_id ),
'comment_status' => 'approved',
],
Reviews::get_reviews_page_url()
)
),
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html( $approved_phrase ) : esc_html( $approved_only_phrase )
);
} else {
// Don't link the comment bubble when there are no approved reviews.
printf(
'',
esc_html( $approved_reviews_number ),
$pending_comments ? esc_html__( 'No approved reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
);
}
if ( $pending_comments ) {
printf(
'%s',
esc_url(
add_query_arg(
[
'product_id' => urlencode( $post_id ),
'comment_status' => 'moderated',
],
Reviews::get_reviews_page_url()
)
),
esc_html( $pending_reviews_number ),
esc_html( $pending_phrase )
);
} else {
printf(
'%s',
esc_html( $pending_reviews_number ),
$approved_review_count ? esc_html__( 'No pending reviews', 'woocommerce' ) : esc_html__( 'No reviews', 'woocommerce' )
);
}
}
}