Files
folio/src/CommentManager.php
T

203 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
class CommentManager
{
public function __construct(private PDO $pdo)
{
}
/**
* Enregistre un commentaire non vérifié.
* Retourne ['token' => 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<int, array<string, mixed>> */
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<string, mixed>|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<int, array<string, mixed>>
*/
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);
}
}
}