mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
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:
1
TODO.md
1
TODO.md
@@ -26,3 +26,4 @@
|
||||
- [x] Add xamxam@erg.be mailto link at top of student (partage) form
|
||||
- [x] On validation error, append "envoyez un e-mail à xamxam@erg.be" to flash error message
|
||||
- [x] Preserve uploaded file names across validation redirects: store in session, display as warning on re-render so the student knows which files to re-select
|
||||
- [x] Obfuscate all email addresses and mailto: links as HTML decimal entities site-wide (EmailObfuscator class, applied in templates + Parsedown post-processing)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
require_once __DIR__ . '/../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
require_once __DIR__ . '/../../src/ShareLink.php';
|
||||
require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
|
||||
App::adminGuard();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
|
||||
class AboutController
|
||||
{
|
||||
@@ -35,7 +36,7 @@ class AboutController
|
||||
|
||||
return [
|
||||
'currentNav' => 'apropos',
|
||||
'aboutHtml' => $pd->text($rawContent),
|
||||
'aboutHtml' => EmailObfuscator::obfuscateHtml($pd->text($rawContent)),
|
||||
'contacts' => $contacts,
|
||||
'pageTitle' => 'À Propos – XAMXAM',
|
||||
'metaDescription' => "À propos de XAMXAM, le répertoire des mémoires de fin d'études de l'erg – École de Recherches Graphiques de Bruxelles.",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
|
||||
class LicenceController
|
||||
{
|
||||
@@ -26,7 +27,7 @@ class LicenceController
|
||||
|
||||
$pd = new Parsedown();
|
||||
$pd->setSafeMode(true);
|
||||
$html = $pd->text($content);
|
||||
$html = EmailObfuscator::obfuscateHtml($pd->text($content));
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
|
||||
@@ -185,6 +185,8 @@ class Dispatcher
|
||||
*/
|
||||
private function render(string $view, array $vars): void
|
||||
{
|
||||
require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
|
||||
$viewPath = APP_ROOT . '/templates/' . $view . '.php';
|
||||
if (!file_exists($viewPath)) {
|
||||
http_response_code(500);
|
||||
|
||||
119
app/src/EmailObfuscator.php
Normal file
119
app/src/EmailObfuscator.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* EmailObfuscator
|
||||
*
|
||||
* Obfuscates email addresses as HTML decimal entities so they render
|
||||
* correctly in browsers but are invisible to most bots and scrapers.
|
||||
*
|
||||
* Example:
|
||||
* obfuscateEmail('foo@example.com')
|
||||
* → foo@example.com
|
||||
*
|
||||
* obfuscateMailto('foo@example.com')
|
||||
* → mailto:foo@ex...
|
||||
* (usable inside href="...")
|
||||
*
|
||||
* obfuscateEmailText('Contact xamxam@erg.be for help')
|
||||
* → 'Contact xa...@er... for help'
|
||||
* (replaces email addresses found in plain text)
|
||||
*
|
||||
* Usage:
|
||||
* require_once APP_ROOT . '/src/EmailObfuscator.php';
|
||||
* echo '<a href="' . EmailObfuscator::mailto('name@domain.com') . '">' . EmailObfuscator::email('name@domain.com') . '</a>';
|
||||
*/
|
||||
|
||||
class EmailObfuscator
|
||||
{
|
||||
/**
|
||||
* Obfuscate an email address for display in HTML content.
|
||||
*/
|
||||
public static function email(string $address): string
|
||||
{
|
||||
return self::encode($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obfuscate a mailto: link for use in an href attribute.
|
||||
* Returns 'mailto:' followed by the obfuscated email.
|
||||
*/
|
||||
public static function mailto(string $address): string
|
||||
{
|
||||
return 'mailto:' . self::encode($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace plain-text email addresses in a string with obfuscated versions.
|
||||
* Only replaces addresses that appear as bare text (not already inside HTML entities).
|
||||
*/
|
||||
public static function emailText(string $text): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/',
|
||||
fn(array $m) => self::encode($m[0]),
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obfuscate a mailto: URL found in text (e.g. from Markdown or user input).
|
||||
* Matches 'mailto:user@domain.com' pattern.
|
||||
*/
|
||||
public static function mailtoInText(string $text): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/mailto:([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/i',
|
||||
fn(array $m) => 'mailto:' . self::encode($m[1]),
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-process rendered HTML: obfuscate all mailto: links and their
|
||||
* visible text (email addresses) so bots can't scrape them.
|
||||
*
|
||||
* Matches patterns like:
|
||||
* <a href="mailto:foo@bar.com">foo@bar.com</a>
|
||||
* <a href="mailto:foo@bar.com">some text</a>
|
||||
*
|
||||
* In the second case, only the href is obfuscated (the link text is kept).
|
||||
*/
|
||||
public static function obfuscateHtml(string $html): string
|
||||
{
|
||||
// Match <a> tags whose href starts with mailto:
|
||||
return preg_replace_callback(
|
||||
'/<a\s[^>]*href="mailto:([^"]+)"[^>]*>(.*?)<\/a>/is',
|
||||
function (array $m): string {
|
||||
$email = $m[1];
|
||||
$linkText = $m[2];
|
||||
$obfuscated = self::encode($email);
|
||||
|
||||
// If the link text is the email itself, replace it too
|
||||
$text = strip_tags($linkText);
|
||||
if ($text === $email) {
|
||||
$linkText = self::encode($linkText);
|
||||
}
|
||||
|
||||
// Rebuild the tag with obfuscated values
|
||||
return str_replace(
|
||||
['mailto:' . $email, '>' . $m[2] . '<'],
|
||||
['mailto:' . $obfuscated, '>' . $linkText . '<'],
|
||||
$m[0]
|
||||
);
|
||||
},
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
// ── Private ───────────────────────────────────────────────────────────
|
||||
|
||||
private static function encode(string $s): string
|
||||
{
|
||||
$out = '';
|
||||
$len = strlen($s);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$out .= '&#' . ord($s[$i]) . ';';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB |
@@ -181,6 +181,19 @@
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: kvyyvksn c5873f06 "fix: add help email, preserve file names on validation error, license fix" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: kvyyvksn c5873f06 "fix: add help email, preserve file names on validation error, license fix" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: roqtyzln d714ae9b "feat: obfuscate all email addresses and mailto links as HTML entities" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: roqtyzln 34d91340 "feat: obfuscate all email addresses and mailto links as HTML entities" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||
@@ -367,9 +380,7 @@
|
||||
<div class="access-req-card__info">
|
||||
<div>
|
||||
<strong>Email :</strong>
|
||||
<a href="mailto:<?= htmlspecialchars($req['email']) ?>">
|
||||
<?= htmlspecialchars($req['email']) ?>
|
||||
</a>
|
||||
<a href="<?= EmailObfuscator::mailto($req['email']) ?>"><?= htmlspecialchars($req['email']) ?></a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date :</strong>
|
||||
|
||||
@@ -59,9 +59,7 @@
|
||||
<div class="access-req-card__info">
|
||||
<div>
|
||||
<strong>Email :</strong>
|
||||
<a href="mailto:<?= htmlspecialchars($req['email']) ?>">
|
||||
<?= htmlspecialchars($req['email']) ?>
|
||||
</a>
|
||||
<a href="<?= EmailObfuscator::mailto($req['email']) ?>"><?= htmlspecialchars($req['email']) ?></a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date :</strong>
|
||||
|
||||
@@ -110,10 +110,12 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
||||
$flashError = $_SESSION["_flash_error"] ?? null;
|
||||
$flashWarning = $_SESSION["_flash_warning"] ?? null;
|
||||
$flashSuccess = $_SESSION["_flash_success"] ?? null;
|
||||
$flashContact = $_SESSION["_flash_contact"] ?? false;
|
||||
unset(
|
||||
$_SESSION["_flash_error"],
|
||||
$_SESSION["_flash_warning"],
|
||||
$_SESSION["_flash_success"],
|
||||
$_SESSION["_flash_contact"],
|
||||
);
|
||||
?>
|
||||
<?php if ($flashError): ?>
|
||||
@@ -127,6 +129,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? [];
|
||||
) ?></div>
|
||||
<script>document.addEventListener('DOMContentLoaded',function(){var el=document.getElementById('flash-warning');if(el){el.scrollIntoView({behavior:'smooth',block:'center'});el.focus();}});</script>
|
||||
<?php endif; ?>
|
||||
<?php if ($flashContact && $mode === 'partage'): ?>
|
||||
<?php require_once APP_ROOT . '/src/EmailObfuscator.php'; ?>
|
||||
<div class="flash-info" role="alert">
|
||||
Si le problème persiste, envoyez un e-mail à
|
||||
<a href="<?= EmailObfuscator::mailto('xamxam@erg.be') ?>"><?= EmailObfuscator::email('xamxam@erg.be') ?></a>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($flashSuccess): ?>
|
||||
<div class="flash-success" role="alert"><?= htmlspecialchars(
|
||||
$flashSuccess,
|
||||
|
||||
@@ -83,9 +83,7 @@ function renderEntries(array $entries): string
|
||||
fn($e) => !empty($e),
|
||||
);
|
||||
foreach ($emails as $email): ?>
|
||||
<a href="mailto:<?= htmlspecialchars(
|
||||
$email,
|
||||
) ?>"><?= htmlspecialchars($email) ?></a>
|
||||
<a href="<?= EmailObfuscator::mailto($email) ?>"><?= htmlspecialchars($email) ?></a>
|
||||
<?php endforeach;
|
||||
?>
|
||||
</address>
|
||||
|
||||
@@ -252,9 +252,7 @@
|
||||
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
|
||||
</a>
|
||||
<?php elseif ($_isEmail): ?>
|
||||
<a href="mailto:<?= htmlspecialchars(
|
||||
$_contact,
|
||||
) ?>"><?= htmlspecialchars($_contact) ?></a>
|
||||
<a href="<?= EmailObfuscator::mailto($_contact) ?>"><?= htmlspecialchars($_contact) ?></a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($_contact) ?>
|
||||
<?php endif;
|
||||
|
||||
Reference in New Issue
Block a user