feat: obfuscate all email addresses and mailto links as HTML entities

Added EmailObfuscator class (src/EmailObfuscator.php) that converts
email addresses to HTML decimal entities (e.g. foo@...)
so browsers render them correctly but bots and scrapers see gibberish.

Methods:
- email($addr): obfuscate for display in HTML content
- mailto($addr): return obfuscated mailto: href
- obfuscateHtml($html): post-process rendered HTML to obfuscate all
  mailto: links (used after Parsedown/Markdown rendering)

Applied to:
- partage/index.php: mailto link at top + error scenarios via _flash_contact
  flag rendered in form.php (outside htmlspecialchars to avoid double-escape)
- admin/acces.php: request email mailto links
- admin/file-access.php: request email mailto links
- public/about.php: contact email mailto links
- public/tfe.php: author contact mailto links
- AboutController: Parsedown output post-processing
- LicenceController: Parsedown output post-processing
- Dispatcher::render(): require_once EmailObfuscator for all public views

Also fixed _flash_contact session flag in form.php partial to show
contact email line on share link validation errors (separate from
flash_error/warning to bypass htmlspecialchars double-escaping).
This commit is contained in:
Pontoporeia
2026-05-10 14:51:37 +02:00
parent ab6e266807
commit 38dc8de9d8
14 changed files with 159 additions and 19 deletions

View File

@@ -8,6 +8,7 @@
* /partage/recapitulatif.php?id=N - Post-submission confirmation page
*/
require_once __DIR__ . '/../../bootstrap.php';
require_once APP_ROOT . '/src/EmailObfuscator.php';
// Parse the requested path from REQUEST_URI
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
@@ -380,7 +381,7 @@ function renderShareLinkForm(string $slug, array $link): void
<div class="share-help-contact">
<p>Des questions ou un problème avec le formulaire ?
<a href="mailto:xamxam@erg.be">xamxam@erg.be</a></p>
<a href="<?= EmailObfuscator::mailto('xamxam@erg.be') ?>"><?= EmailObfuscator::email('xamxam@erg.be') ?></a></p>
</div>
<?php
@@ -527,8 +528,8 @@ function handleShareLinkSubmission(string $slug): void
// Repopulate the form and surface a clear warning to the student.
// Store as plain text — htmlspecialchars() is applied at render time.
$_SESSION['_flash_warning'] = 'Votre soumission ressemble à un TFE déjà enregistré.'
. "\n" . $e->existingIdentifier . ' — ' . $e->existingTitle . ' (' . $e->existingYear . ')'
. "\nSi vous pensez qu'il s'agit d'une erreur, vous pouvez contacter l'équipe à xamxam@erg.be.";
. "\n" . $e->existingIdentifier . ' — ' . $e->existingTitle . ' (' . $e->existingYear . ')';
$_SESSION['_flash_contact'] = true;
$_SESSION['form_data_share_' . $slug] = $_POST;
storePrimedFiles($slug);
$_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32)); // Regenerate token
@@ -544,8 +545,8 @@ function handleShareLinkSubmission(string $slug): void
]);
ErrorHandler::log('partage_submit', $e, ['slug' => $slug, 'author' => $authorName]);
$_SESSION['_flash_error'] = ErrorHandler::userMessage($e)
. "\n\nSi le problème persiste, envoyez un e-mail à xamxam@erg.be.";
$_SESSION['_flash_error'] = ErrorHandler::userMessage($e);
$_SESSION['_flash_contact'] = true;
$_SESSION['form_data_share_' . $slug] = $_POST;
storePrimedFiles($slug);
$_SESSION[$shareCsrfKey] = bin2hex(random_bytes(32)); // Regenerate token