mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
feat: email retry page on 550 rejection; confirmation_email optional in admin form
This commit is contained in:
3
TODO.md
3
TODO.md
@@ -19,6 +19,9 @@
|
||||
|
||||
## Bug fixes
|
||||
- [x] **smtp-test.php** — wrap `SmtpRelay::send()` in `try/catch SmtpSendException` so SMTP delivery failures (e.g. 550 recipient rejected) surface as a proper flash error instead of an uncaught exception/silent crash
|
||||
- [x] **partage email retry** — on 550 recipient-rejected, redirect to `/partage/retry-email` instead of `recapitulatif`; student can correct address and resend or skip
|
||||
- [x] **ThesisCreateController** — `confirmation_email` is now optional (empty = skip send)
|
||||
- [x] **admin/add.php template** — email confirmation field marked optional, label and hint updated
|
||||
|
||||
## Previously completed
|
||||
- [x] Multi-file upload for thesis files (basic)
|
||||
|
||||
@@ -1008,3 +1008,104 @@ a.recap-file-name:hover {
|
||||
.form-help-block ol { margin: 0 0 var(--space-xs); padding-left: var(--space-m); }
|
||||
.form-help-block li { margin-bottom: var(--space-3xs); }
|
||||
.form-help-block a { color: var(--accent-primary); }
|
||||
|
||||
/* ── E-mail retry page ───────────────────────────────────────────────────── */
|
||||
|
||||
.partage-retry-email {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-l);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.retry-email-section {
|
||||
border-top: 1px solid var(--border-primary);
|
||||
padding-top: var(--space-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
|
||||
.retry-email-section h2 {
|
||||
font-size: var(--step-0);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.retry-smtp-detail {
|
||||
display: block;
|
||||
margin-top: var(--space-2xs);
|
||||
font-size: var(--step--2);
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.retry-email-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
|
||||
.retry-email-form .field-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
}
|
||||
|
||||
.retry-email-form label {
|
||||
font-size: var(--step--1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.retry-email-form input[type="email"] {
|
||||
padding: var(--space-xs) var(--space-s);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 4px;
|
||||
font-size: var(--step-0);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.retry-email-form input.input-error {
|
||||
border-color: var(--error, #c00);
|
||||
}
|
||||
|
||||
.retry-email-actions {
|
||||
display: flex;
|
||||
gap: var(--space-s);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: var(--space-xs) var(--space-m);
|
||||
background: var(--accent-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: var(--step-0);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-secondary, var(--accent-primary));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: var(--space-xs) var(--space-m);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 4px;
|
||||
font-size: var(--step-0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: color-mix(in srgb, var(--text-secondary) 8%, transparent);
|
||||
}
|
||||
|
||||
@@ -447,9 +447,6 @@ function handleShareLinkSubmission(string $slug): void
|
||||
'share_slug' => $slug,
|
||||
]);
|
||||
|
||||
// Send confirmation e-mail (non-blocking; failure doesn't stop redirect)
|
||||
$emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST);
|
||||
|
||||
// Mark the link as used
|
||||
$shareLinkModel = new ShareLink(Database::getInstance());
|
||||
$shareLinkModel->incrementUsage($link['id']);
|
||||
@@ -457,7 +454,22 @@ function handleShareLinkSubmission(string $slug): void
|
||||
// Clean up share-specific session data
|
||||
unset($_SESSION[$shareCsrfKey]);
|
||||
unset($_SESSION['share_verified_' . $slug]);
|
||||
|
||||
// Send confirmation e-mail — on delivery failure, redirect to retry page
|
||||
$emailError = null;
|
||||
try {
|
||||
$emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST);
|
||||
$_SESSION['share_email_sent'] = $emailSent;
|
||||
} catch (SmtpSendException $e) {
|
||||
if ($e->isRecipientRejected()) {
|
||||
$_SESSION['share_email_retry_thesis'] = $thesisId;
|
||||
$_SESSION['share_email_retry_error'] = $e->smtpResponse;
|
||||
header('Location: /partage/retry-email?id=' . urlencode((string)$thesisId));
|
||||
exit();
|
||||
}
|
||||
// Non-recipient errors (relay down, etc.) — skip email silently
|
||||
$_SESSION['share_email_sent'] = false;
|
||||
}
|
||||
|
||||
// Redirect to thanks page
|
||||
header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId));
|
||||
|
||||
122
app/public/partage/retry-email.php
Normal file
122
app/public/partage/retry-email.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* E-mail retry page for share-link submissions.
|
||||
*
|
||||
* Shown when the confirmation e-mail bounced with a 550 recipient-rejected
|
||||
* error. The student can correct their address and resend, or skip.
|
||||
*/
|
||||
// Always boot — this page is a direct-response route (loaded before
|
||||
// Dispatcher calls App::boot()), so we must start the session ourselves.
|
||||
App::boot();
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
require_once APP_ROOT . '/src/StudentEmail.php';
|
||||
|
||||
$thesisId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
// Guard: only allow access when the session retry token matches
|
||||
$sessionThesisId = $_SESSION['share_email_retry_thesis'] ?? null;
|
||||
if ($thesisId <= 0 || $sessionThesisId !== $thesisId) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
$smtpError = $_SESSION['share_email_retry_error'] ?? '';
|
||||
|
||||
// ── POST: retry send ──────────────────────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Skip button
|
||||
if (isset($_POST['skip'])) {
|
||||
unset($_SESSION['share_email_retry_thesis'], $_SESSION['share_email_retry_error']);
|
||||
$_SESSION['share_email_sent'] = false;
|
||||
header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId));
|
||||
exit;
|
||||
}
|
||||
|
||||
$newEmail = trim($_POST['confirmation_email'] ?? '');
|
||||
$inputError = null;
|
||||
|
||||
if ($newEmail === '' || filter_var($newEmail, FILTER_VALIDATE_EMAIL) === false) {
|
||||
$inputError = 'Veuillez saisir une adresse e-mail valide.';
|
||||
} else {
|
||||
$db = Database::getInstance();
|
||||
try {
|
||||
$sent = StudentEmail::sendConfirmation($db, $thesisId, ['confirmation_email' => $newEmail]);
|
||||
unset($_SESSION['share_email_retry_thesis'], $_SESSION['share_email_retry_error']);
|
||||
$_SESSION['share_email_sent'] = $sent;
|
||||
header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId));
|
||||
exit;
|
||||
} catch (SmtpSendException $e) {
|
||||
// Update the displayed error with the fresh response
|
||||
$smtpError = $e->smtpResponse;
|
||||
$inputError = 'Le serveur a de nouveau rejeté cette adresse. Vérifiez l\'adresse et réessayez.';
|
||||
$_SESSION['share_email_retry_error'] = $smtpError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = 'Corriger l\'adresse e-mail';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($pageTitle) ?></title>
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/assets/favicon/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/assets/favicon/apple-touch-icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
|
||||
<link rel="shortcut icon" href="/assets/favicon/favicon.ico">
|
||||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/common.css') ?>">
|
||||
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form.css') ?>">
|
||||
</head>
|
||||
<body class="student-body">
|
||||
<main id="main-content" class="partage-retry-email">
|
||||
|
||||
<div class="thanks-success">
|
||||
<h1>✅ Votre TFE a bien été enregistré !</h1>
|
||||
<p class="thanks-message">Il y a cependant eu un problème lors de l'envoi de l'e-mail de confirmation.</p>
|
||||
</div>
|
||||
|
||||
<section class="retry-email-section">
|
||||
<h2>Corriger l'adresse e-mail</h2>
|
||||
|
||||
<div class="flash-error" role="alert">
|
||||
<strong>L'adresse e-mail a été rejetée par le serveur.</strong><br>
|
||||
<?php if ($smtpError !== ''): ?>
|
||||
<code class="retry-smtp-detail"><?= htmlspecialchars($smtpError) ?></code>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($inputError)): ?>
|
||||
<div class="flash-error" role="alert"><?= htmlspecialchars($inputError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>Votre TFE est enregistré — vous pouvez corriger votre adresse ci-dessous pour recevoir le récapitulatif, ou continuer sans e-mail.</p>
|
||||
|
||||
<form method="post" action="/partage/retry-email?id=<?= urlencode((string)$thesisId) ?>" class="retry-email-form">
|
||||
<div class="field-wrap">
|
||||
<label for="confirmation_email">Adresse e-mail corrigée</label>
|
||||
<input
|
||||
type="email"
|
||||
id="confirmation_email"
|
||||
name="confirmation_email"
|
||||
placeholder="ton.email@exemple.be"
|
||||
autofocus
|
||||
required
|
||||
class="<?= !empty($inputError) ? 'input-error' : '' ?>"
|
||||
>
|
||||
</div>
|
||||
<div class="retry-email-actions">
|
||||
<button type="submit" class="btn-primary">Renvoyer l'e-mail</button>
|
||||
<button type="submit" name="skip" value="1" class="btn-secondary">Continuer sans e-mail</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -325,15 +325,14 @@ class ThesisCreateController
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmation e-mail (required)
|
||||
// Confirmation e-mail (optional)
|
||||
$confirmationEmail = trim($post['confirmation_email'] ?? '');
|
||||
if ($confirmationEmail === '') {
|
||||
throw new Exception("L'adresse e-mail de confirmation est requise.");
|
||||
}
|
||||
if ($confirmationEmail !== '') {
|
||||
$confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL);
|
||||
if ($confirmationEmail === false) {
|
||||
throw new Exception("L'adresse e-mail de confirmation n'est pas valide.");
|
||||
}
|
||||
}
|
||||
|
||||
return compact(
|
||||
'auteurName', 'mail', 'showContact', 'confirmationEmail', 'annee', 'orientationId', 'apProgramId',
|
||||
|
||||
@@ -122,6 +122,13 @@ class Dispatcher {
|
||||
};
|
||||
}
|
||||
|
||||
// /partage/retry-email (GET: show retry form, POST: resend)
|
||||
if ($path === '/partage/retry-email') {
|
||||
return function() {
|
||||
require APP_ROOT . '/public/partage/retry-email.php';
|
||||
};
|
||||
}
|
||||
|
||||
// /partage/*
|
||||
if (preg_match('#^/partage(/.*)?$#', $path)) {
|
||||
return function() {
|
||||
|
||||
@@ -102,9 +102,8 @@ class StudentEmail {
|
||||
try {
|
||||
$result = SmtpRelay::send($db, $to, $subject, $htmlBody);
|
||||
} catch (SmtpSendException $e) {
|
||||
// Confirmation email failure must not abort the successful submission.
|
||||
error_log("[StudentEmail] SMTP error sending to {$to} for thesis #{$thesisId}: " . $e->getMessage());
|
||||
return false;
|
||||
throw $e; // re-throw so callers can react (e.g. redirect to retry page)
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":18,"identifier":"2025-015","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T10:13:43+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":19,"identifier":"2025-016","author":"Lila Dubois, Karim Nassar","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:27:07+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":20,"identifier":"2025-017","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:37:11+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":21,"identifier":"2025-018","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:41:38+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":22,"identifier":"2025-019","author":"Lila Dubois, Karim Nassar","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:45:36+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
{"source":"partage","action":"submit","status":"success","thesis_id":23,"identifier":"2025-020","author":"Zoé Lambert","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:46:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<!-- ═══════════════════ E-mail de confirmation ═══════════════ -->
|
||||
<fieldset>
|
||||
<legend>E-mail de confirmation</legend>
|
||||
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail * :'; $value = old('confirmation_email'); $type = 'email'; $required = true; $placeholder = 'ton.email@exemple.be'; $hint = 'Nécessaire pour recevoir le récapitulatif de ta soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail :'; $value = old('confirmation_email'); $type = 'email'; $required = false; $placeholder = 'ton.email@exemple.be'; $hint = 'Optionnel — pour envoyer un récapitulatif de la soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-footer">
|
||||
|
||||
Reference in New Issue
Block a user