0, ], $atts); $download_id = (int) ($atts['id'] ?? 0); if (!$download_id) { global $post; $download_id = $post ? (int) $post->ID : 0; } if (!$download_id) { return ''; } return self::render_reviews($download_id); } public static function render_reviews(int $download_id): string { if (get_post_type($download_id) !== DownloadPostType::POST_TYPE) { if (defined('REST_REQUEST') && REST_REQUEST) { return '
' . esc_html__('Reviews are only available on Download products.', '2download-connector') . '
'; } return ''; } $notice_ok = false; // Only honor notice query params when accompanied by a valid nonce. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the nonce itself for verification. $notice_nonce = isset($_GET[self::NOTICE_NONCE_PARAM]) ? sanitize_text_field((string) wp_unslash($_GET[self::NOTICE_NONCE_PARAM])) : ''; if ($notice_nonce !== '' && wp_verify_nonce($notice_nonce, self::NOTICE_NONCE_ACTION)) { $notice_ok = true; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified notice nonce above. $submitted = $notice_ok && isset($_GET['ToDownload_review_submitted']) && sanitize_key((string) wp_unslash($_GET['ToDownload_review_submitted'])) === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Gated by verified notice nonce above. $error_raw = ($notice_ok && isset($_GET['ToDownload_review_error'])) ? sanitize_text_field((string) wp_unslash($_GET['ToDownload_review_error'])) : ''; $error = $error_raw !== '' ? sanitize_text_field(rawurldecode($error_raw)) : ''; $comments = get_comments([ 'post_id' => $download_id, 'type' => self::COMMENT_TYPE, 'status' => 'approve', 'number' => 20, 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ]); $avg = self::average_rating($download_id); $count = is_array($comments) ? count($comments) : 0; ob_start(); echo '
'; echo '

' . esc_html__('Reviews', '2download-connector') . '

'; if ($submitted) { echo '

' . esc_html__('Thanks! Your review was submitted.', '2download-connector') . '

'; } elseif ($error !== '') { echo '

' . esc_html($error) . '

'; } echo '

'; if ($avg > 0) { echo esc_html(number_format($avg, 1)) . ' / 5'; } else { echo esc_html__('No ratings yet', '2download-connector'); } /* translators: %d = number of reviews */ echo ' · ' . esc_html(sprintf(_n('%d review', '%d reviews', $count, '2download-connector'), $count)); echo '

'; if (!empty($comments)) { echo ''; } // Only show the review CTA/form after a completed purchase. if (is_user_logged_in()) { $user = wp_get_current_user(); $user_id = $user instanceof \WP_User ? (int) $user->ID : 0; $email = $user instanceof \WP_User ? (string) $user->user_email : ''; if ($user_id > 0 && $email !== '' && is_email($email)) { // Covers cases where purchase happened before login. Orders::link_customer_user_by_email($email, $user_id); } if ($user_id > 0 && Orders::user_has_purchased_download($user_id, $download_id)) { // Outputs a complete
and related markup (safe internal renderer). // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo self::render_form($download_id); } } echo '
'; return (string) ob_get_clean(); } private static function render_form(int $download_id): string { $current_user = wp_get_current_user(); $is_logged_in = is_user_logged_in(); $prefill_email = ''; $prefill_name = ''; if ($is_logged_in && $current_user instanceof \WP_User) { $prefill_email = (string) $current_user->user_email; $prefill_name = (string) $current_user->display_name; } ob_start(); echo '
'; echo '

' . esc_html__('Leave a review', '2download-connector') . '

'; if (!$is_logged_in) { $login_url = wp_login_url(get_permalink($download_id)); echo '

' . esc_html__('Only customers who purchased this product can leave a review.', '2download-connector') . '

'; echo '

' . esc_html__('Log in to leave a review', '2download-connector') . '

'; echo '
'; return (string) ob_get_clean(); } echo ''; wp_nonce_field('todownload_submit_review', 'ToDownload_review_nonce'); echo ''; /* translators: 1: user display name, 2: user email */ echo '

' . esc_html(sprintf(__('Reviewing as %1$s (%2$s)', '2download-connector'), $prefill_name ?: __('User', '2download-connector'), $prefill_email)) . '

'; echo '


'; echo '

'; echo '


'; echo '

'; echo '

'; echo ''; echo ''; return (string) ob_get_clean(); } public static function handle_submission(): void { $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper(sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_METHOD']))) : ''; if ($method !== 'POST') { return; } $nonce = isset($_POST['ToDownload_review_nonce']) ? sanitize_text_field((string) wp_unslash($_POST['ToDownload_review_nonce'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'todownload_submit_review')) { return; } if (empty($_POST['ToDownload_review_post_id'])) { return; } $download_id = absint(wp_unslash($_POST['ToDownload_review_post_id'])); if (!$download_id || get_post_type($download_id) !== DownloadPostType::POST_TYPE) { return; } if (!is_user_logged_in()) { self::bounce($download_id, 'Please log in to leave a review.'); } $rating = isset($_POST['ToDownload_review_rating']) ? absint(wp_unslash($_POST['ToDownload_review_rating'])) : 0; $rating = max(1, min(5, $rating)); $content = sanitize_textarea_field((string) wp_unslash($_POST['ToDownload_review_content'] ?? '')); $content = trim($content); if ($content === '') { self::bounce($download_id, 'Please write a review.'); } $user = wp_get_current_user(); $user_id = $user instanceof \WP_User ? (int) $user->ID : 0; $author = $user instanceof \WP_User ? (string) $user->display_name : ''; $email = $user instanceof \WP_User ? (string) $user->user_email : ''; if ($user_id <= 0) { self::bounce($download_id, 'Please log in to leave a review.'); } if ($email === '' || !is_email($email)) { self::bounce($download_id, 'Your account email is missing or invalid.'); } // Link customer record by email to the current WP user (covers cases where purchase happened before login). Orders::link_customer_user_by_email($email, $user_id); if (!Orders::user_has_purchased_download($user_id, $download_id)) { self::bounce($download_id, 'A completed purchase is required to review this item.'); } $existing = get_comments([ 'post_id' => $download_id, 'type' => self::COMMENT_TYPE, 'user_id' => $user_id, 'number' => 1, 'status' => 'all', ]); $commentdata = [ 'comment_post_ID' => $download_id, 'comment_content' => $content, 'comment_type' => self::COMMENT_TYPE, 'comment_author' => $author !== '' ? $author : __('Anonymous', '2download-connector'), 'comment_author_email' => $email, 'user_id' => $user_id, 'comment_approved' => 0, ]; if (is_array($existing) && !empty($existing) && $existing[0] instanceof \WP_Comment) { $commentdata['comment_ID'] = (int) $existing[0]->comment_ID; $comment_id = wp_update_comment($commentdata); $comment_id = $commentdata['comment_ID']; } else { $comment_id = wp_insert_comment($commentdata); } if (!$comment_id || is_wp_error($comment_id)) { self::bounce($download_id, 'Could not save your review.'); } update_comment_meta((int) $comment_id, self::META_RATING, $rating); $redirect = add_query_arg([ 'ToDownload_review_submitted' => '1', self::NOTICE_NONCE_PARAM => wp_create_nonce(self::NOTICE_NONCE_ACTION), ], wp_get_referer() ?: get_permalink($download_id)); wp_safe_redirect($redirect); exit; } private static function bounce(int $download_id, string $message): void { $redirect = add_query_arg([ 'ToDownload_review_error' => rawurlencode($message), self::NOTICE_NONCE_PARAM => wp_create_nonce(self::NOTICE_NONCE_ACTION), ], wp_get_referer() ?: get_permalink($download_id)); wp_safe_redirect($redirect); exit; } private static function average_rating(int $download_id): float { $comments = get_comments([ 'post_id' => $download_id, 'type' => self::COMMENT_TYPE, 'status' => 'approve', 'number' => 200, 'fields' => 'ids', ]); if (!is_array($comments) || empty($comments)) { return 0.0; } $sum = 0; $count = 0; foreach ($comments as $cid) { $rating = (int) get_comment_meta((int) $cid, self::META_RATING, true); if ($rating < 1 || $rating > 5) { continue; } $sum += $rating; $count++; } if ($count === 0) { return 0.0; } return (float) round($sum / $count, 2); } private static function stars(int $rating): string { $rating = max(0, min(5, $rating)); $filled = str_repeat('★', $rating); $empty = str_repeat('☆', 5 - $rating); return $filled . $empty; } }