feat : magic link confirm, notif auteur, rate-limit IP, duplicate, cache MD, lazy img (v1.6.18)

- magic.php : GET=confirmation page, POST=consommation (protège vs scanners) (#27)
- verify_comment : email de notification à l'auteur de l'article (#44)
- login/index.php : rate limit par IP (MAGIC_MAX_PER_IP_HOUR=10) (#23)
- ArticleManager::duplicate() + route POST /duplicate/{uuid} + bouton ⧉ admin/articles (#7)
- post_view.php : cache JSON du rendu Markdown (invalidé sur mtime index.md) (#17)
- post_view.php : loading="lazy" sur toutes les <img> du contenu (#21)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 10:30:55 +02:00
parent 51055b7321
commit 11399a54a6
9 changed files with 201 additions and 43 deletions
+55 -1
View File
@@ -1122,6 +1122,27 @@ switch ($action) {
header('Location: /edit/' . rawurlencode($uuid));
exit;
case 'duplicate':
requireAuth();
if ($uuid !== '' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$srcArticle = $articles->getByUuid($uuid);
if (!$srcArticle) {
header('Location: /admin/articles');
exit;
}
if (!isAdmin() && ($srcArticle['author'] ?? '') !== (currentUserEmail() ?? '')) {
http_response_code(403);
exit;
}
$newUuid = $articles->duplicate($uuid, currentUserEmail() ?? '');
if ($newUuid) {
header('Location: /edit/' . rawurlencode($newUuid) . '/1');
exit;
}
}
header('Location: /admin/articles');
exit;
case 'delete':
requireAuth();
if ($uuid !== '') {
@@ -2170,11 +2191,44 @@ switch ($action) {
$pdo = dbPdo();
if ($pdo && preg_match('/^[0-9]{6}$/', $vcCode)) {
require_once BASE_PATH . '/src/CommentManager.php';
$cm = new CommentManager($pdo);
$cm = new CommentManager($pdo);
// Récupère les données du commentaire avant vérification (le token est effacé après)
$vcPreSt = $pdo->prepare(
'SELECT author_name, content FROM comments WHERE verify_token = :t AND verified = FALSE LIMIT 1'
);
$vcPreSt->execute([':t' => $vcToken]);
$vcPreInfo = $vcPreSt->fetch(PDO::FETCH_ASSOC) ?: null;
$result = $cm->verify($vcToken, $vcCode);
if (is_string($result)) {
$vcArticle = $articles->getByUuid($result);
$vcSlug = $vcArticle ? ($vcArticle['slug'] ?? $result) : $result;
// Notification email à l'auteur de l'article
$vcAuthorEmail = $vcArticle['author'] ?? '';
if ($vcAuthorEmail !== '' && $vcPreInfo) {
require_once BASE_PATH . '/src/mailer.php';
$vcPostUrl = rtrim(APP_URL, '/') . '/post/' . rawurlencode($vcSlug) . '#comments';
$vcAdminUrl = rtrim(APP_URL, '/') . '/admin/comments';
$vcExcerpt = mb_strimwidth(trim((string)$vcPreInfo['content']), 0, 200, '…');
$vcSubject = '[' . siteTitle() . '] Nouveau commentaire sur « ' . ($vcArticle['title'] ?? '') . ' »';
$vcHtml = '<!DOCTYPE html><html><body style="font-family:sans-serif;max-width:560px;margin:0 auto">'
. '<p>Bonjour,</p>'
. '<p><strong>' . htmlspecialchars((string)$vcPreInfo['author_name']) . '</strong>'
. ' a commenté votre article <em>' . htmlspecialchars($vcArticle['title'] ?? '') . '</em> :</p>'
. '<blockquote style="border-left:3px solid #ddd;margin:0;padding:0 1em;color:#555">'
. nl2br(htmlspecialchars($vcExcerpt)) . '</blockquote>'
. '<p><a href="' . htmlspecialchars($vcPostUrl) . '">Voir le commentaire</a>'
. ' &nbsp;·&nbsp; <a href="' . htmlspecialchars($vcAdminUrl) . '">Modérer</a></p>'
. '</body></html>';
try {
envoyer_mail_smtp($vcAuthorEmail, $vcSubject, $vcHtml);
} catch (\RuntimeException) {
// Taux limité ou SMTP indisponible, on ne bloque pas le visiteur
}
}
header('Location: /post/' . rawurlencode($vcSlug) . '?verified=1#comments');
exit;
}