| @@ -1139,8 +1139,38 @@ | ||
| 1139 | 1139 | return max(0, min(5, (int) $value)); |
| 1140 | 1140 | } |
| 1141 | 1141 | |
| 1142 | 1142 | /** |
| 1143 | + * The star ratings a review list is filtered to, from "5,4" or [5, 4]. | |
| 1144 | + * | |
| 1145 | + * Only whole stars 1-5 survive, each once, so the list is never longer than five. | |
| 1146 | + * | |
| 1147 | + * @param string|array $value | |
| 1148 | + * @return int[] | |
| 1149 | + */ | |
| 1150 | + public static function ratingList($value): array | |
| 1151 | + { | |
| 1152 | + $parts = is_array($value) ? $value : explode(',', (string) $value); | |
| 1153 | + $ratings = []; | |
| 1154 | + | |
| 1155 | + foreach (array_slice($parts, 0, 5) as $part) { | |
| 1156 | + $part = is_scalar($part) ? trim((string) $part) : ''; | |
| 1157 | + | |
| 1158 | + if (!ctype_digit($part)) { | |
| 1159 | + continue; | |
| 1160 | + } | |
| 1161 | + | |
| 1162 | + $rating = (int) $part; | |
| 1163 | + | |
| 1164 | + if ($rating >= 1 && $rating <= 5) { | |
| 1165 | + $ratings[$rating] = $rating; | |
| 1166 | + } | |
| 1167 | + } | |
| 1168 | + | |
| 1169 | + return array_values($ratings); | |
| 1170 | + } | |
| 1171 | + | |
| 1172 | + /** | |
| 1143 | 1173 | * Whether the store collects star ratings at all. |
| 1144 | 1174 | * |
| 1145 | 1175 | * Defaults to on, so a store that has never touched the setting shows the |
| 1146 | 1176 | * rating control. |
| @@ -1383,8 +1413,67 @@ | ||
| 1383 | 1413 | 'reviews' => $reviews->toArray(), |
| 1384 | 1414 | ]; |
| 1385 | 1415 | |
| 1386 | 1416 | return apply_filters('fluent_cart/review/public_response', $responseData, $postId); |
| 1417 | + } | |
| 1418 | + | |
| 1419 | + /** | |
| 1420 | + * Resolve an editorial floor before rendering, or for legacy refreshes. | |
| 1421 | + * A client id is context only; integrations authenticating their own tokens | |
| 1422 | + * must verify them before overriding the trusted default. | |
| 1423 | + */ | |
| 1424 | + public static function resolveListMinRating(string $clientId, int $default = 0, int $postId = 0): int | |
| 1425 | + { | |
| 1426 | + $rating = apply_filters('fluent_cart/review/list_min_rating', $default, [ | |
| 1427 | + 'client_id' => $clientId, | |
| 1428 | + 'post_id' => $postId, | |
| 1429 | + ]); | |
| 1430 | + | |
| 1431 | + return is_numeric($rating) && (int) $rating >= 1 && (int) $rating <= 5 | |
| 1432 | + ? (int) $rating : 0; | |
| 1433 | + } | |
| 1434 | + | |
| 1435 | + /** Product-bound, cache-safe list setting; does not grant access to reviews. */ | |
| 1436 | + public static function listRatingToken(int $postId, int $minimum): string | |
| 1437 | + { | |
| 1438 | + $minimum = max(0, min(5, $minimum)); | |
| 1439 | + $payload = 'fcr1-' . $postId . '-' . $minimum; | |
| 1440 | + $signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $payload, wp_salt('auth'), true)), '+/', '-_'), '='); | |
| 1441 | + return 'fcr1-' . $minimum . '-' . $signature; | |
| 1442 | + } | |
| 1443 | + | |
| 1444 | + /** Null means absent, modified, or issued for a different product. */ | |
| 1445 | + public static function minRatingFromToken(int $postId, string $token): ?int | |
| 1446 | + { | |
| 1447 | + if (!preg_match('/^fcr1-([0-5])-[A-Za-z0-9_-]{43}$/D', $token, $matches)) { | |
| 1448 | + return null; | |
| 1449 | + } | |
| 1450 | + $minimum = (int) $matches[1]; | |
| 1451 | + return hash_equals(static::listRatingToken($postId, $minimum), $token) ? $minimum : null; | |
| 1452 | + } | |
| 1453 | + | |
| 1454 | + /** Shared rating visibility for blocks, shortcodes and addon renderers. */ | |
| 1455 | + public static function ratingMeetsThresholds($product, array $attributes = [], $block = null): bool | |
| 1456 | + { | |
| 1457 | + $context = [ | |
| 1458 | + 'product' => $product, | |
| 1459 | + 'post_id' => (int) $product->ID, | |
| 1460 | + 'attributes' => $attributes, | |
| 1461 | + 'block' => $block, | |
| 1462 | + ]; | |
| 1463 | + $minimumCount = max(0, (int) apply_filters( | |
| 1464 | + 'fluent_cart/review/min_review_count', | |
| 1465 | + max(0, (int) Arr::get($attributes, 'minReviewCount', 0)), | |
| 1466 | + $context | |
| 1467 | + )); | |
| 1468 | + $minimumAverage = min(5.0, max(0.0, (float) apply_filters( | |
| 1469 | + 'fluent_cart/review/min_average_rating', | |
| 1470 | + min(5.0, max(0.0, (float) Arr::get($attributes, 'minAverageRating', 0))), | |
| 1471 | + $context | |
| 1472 | + ))); | |
| 1473 | + $info = $product->detail ? ($product->detail->other_info ?: []) : []; | |
| 1474 | + return (int) Arr::get($info, 'review_count', 0) >= $minimumCount | |
| 1475 | + && (float) Arr::get($info, 'average_rating', 0) >= $minimumAverage; | |
| 1387 | 1476 | } |
| 1388 | 1477 | |
| 1389 | 1478 | /** |
| 1390 | 1479 | * Per-request settings cache, shared by every caller of |