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
';
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 '';
return (string) ob_get_clean();
}
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;
}
}