UUID (dans l'URL), 'code' => 6 chiffres (saisi par le visiteur)]. */ public function submit( string $articleUuid, string $name, string $email, string $content, string $ip, string $ua ): array { $bytes = random_bytes(16); $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); $token = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4)); $code = sprintf('%06d', random_int(100000, 999999)); $this->pdo->prepare( 'INSERT INTO comments (article_uuid, author_name, author_email, content, verify_token, verification_code, ip_address, user_agent) VALUES (:uuid, :name, :email, :content, :token, :code, :ip, :ua)' )->execute([ ':uuid' => $articleUuid, ':name' => $name, ':email' => $email, ':content' => $content, ':token' => $token, ':code' => $code, ':ip' => $ip, ':ua' => substr($ua, 0, 512), ]); return ['token' => $token, 'code' => $code]; } /** * Vérifie le code PIN pour un token donné. * Retourne l'article_uuid en cas de succès. * Retourne int > 0 : tentatives restantes (code incorrect). * Retourne 0 : commentaire supprimé après 3 tentatives échouées. * Retourne null : token introuvable ou expiré. */ public function verify(string $token, string $code): string|int|null { $st = $this->pdo->prepare( "SELECT id, verification_code, verify_attempts, article_uuid FROM comments WHERE verify_token = :token AND verified = FALSE AND created_at >= NOW() - INTERVAL '24 hours' LIMIT 1" ); $st->execute([':token' => $token]); $row = $st->fetch(PDO::FETCH_ASSOC); if (!$row) { return null; } if ($row['verification_code'] !== $code) { $newAttempts = (int)$row['verify_attempts'] + 1; if ($newAttempts >= 3) { $this->pdo->prepare('DELETE FROM comments WHERE id = :id') ->execute([':id' => $row['id']]); return 0; } $this->pdo->prepare('UPDATE comments SET verify_attempts = :a WHERE id = :id') ->execute([':a' => $newAttempts, ':id' => $row['id']]); return 3 - $newAttempts; } $this->pdo->prepare( 'UPDATE comments SET verified = TRUE, published = TRUE, verification_code = NULL, verify_token = NULL WHERE id = :id' )->execute([':id' => $row['id']]); return (string)$row['article_uuid']; } /** @return array> */ public function forArticle(string $uuid): array { $st = $this->pdo->prepare( 'SELECT id, author_name, content, created_at FROM comments WHERE article_uuid = :uuid AND verified = TRUE AND published = TRUE ORDER BY created_at ASC' ); $st->execute([':uuid' => $uuid]); return $st->fetchAll(PDO::FETCH_ASSOC); } public function setPublished(int $id, bool $published): void { $this->pdo->prepare('UPDATE comments SET published = :pub WHERE id = :id') ->execute([':pub' => $published, ':id' => $id]); } public function delete(int $id): void { $this->pdo->prepare('DELETE FROM comments WHERE id = :id') ->execute([':id' => $id]); } /** @return array|null */ public function getById(int $id): ?array { $st = $this->pdo->prepare( 'SELECT id, article_uuid, author_name, author_email, content, verify_token, verification_code, verify_attempts, verified, published, created_at, ip_address FROM comments WHERE id = :id LIMIT 1' ); $st->execute([':id' => $id]); $row = $st->fetch(PDO::FETCH_ASSOC); return $row ?: null; } /** @return array{all:int,pending:int,verified:int,hidden:int} */ public function countsByStatus(): array { try { $row = $this->pdo->query( 'SELECT COUNT(*) AS all, COUNT(*) FILTER (WHERE verified = FALSE) AS pending, COUNT(*) FILTER (WHERE verified = TRUE AND published = TRUE) AS verified, COUNT(*) FILTER (WHERE verified = TRUE AND published = FALSE) AS hidden FROM comments' )->fetch(PDO::FETCH_ASSOC); return [ 'all' => (int)($row['all'] ?? 0), 'pending' => (int)($row['pending'] ?? 0), 'verified' => (int)($row['verified'] ?? 0), 'hidden' => (int)($row['hidden'] ?? 0), ]; } catch (\Throwable) { return ['all' => 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0]; } } /** * Retourne tous les commentaires pour l'admin, avec statut email depuis journal_smtp. * * @param string $filterStatus '' = tous, 'pending' = non vérifié, * 'verified' = vérifié+publié, 'hidden' = vérifié+non publié * @return array> */ public function allForAdmin(string $filterStatus = ''): array { $where = match($filterStatus) { 'pending' => 'WHERE c.verified = FALSE', 'verified' => 'WHERE c.verified = TRUE AND c.published = TRUE', 'hidden' => 'WHERE c.verified = TRUE AND c.published = FALSE', default => '', }; $sqlWithJoin = " SELECT c.id, c.article_uuid, c.author_name, c.author_email, c.content, c.verification_code, c.verified, c.published, c.created_at, c.ip_address, j.status AS mail_status, j.error_message AS mail_error, j.sent_at AS mail_sent_at FROM comments c LEFT JOIN LATERAL ( SELECT status, error_message, sent_at FROM journal_smtp WHERE to_email = c.author_email AND created_at BETWEEN c.created_at - INTERVAL '1 minute' AND c.created_at + INTERVAL '10 minutes' ORDER BY created_at ASC LIMIT 1 ) j ON TRUE $where ORDER BY c.created_at DESC "; try { return $this->pdo->query($sqlWithJoin)->fetchAll(PDO::FETCH_ASSOC); } catch (\Throwable) { // journal_smtp absent ou jointure échouée : requête de secours sans jointure $sqlFallback = " SELECT c.id, c.article_uuid, c.author_name, c.author_email, c.content, c.verification_code, c.verified, c.published, c.created_at, c.ip_address, NULL AS mail_status, NULL AS mail_error, NULL AS mail_sent_at FROM comments c $where ORDER BY c.created_at DESC "; return $this->pdo->query($sqlFallback)->fetchAll(PDO::FETCH_ASSOC); } } }