203 lines
7.5 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|