diff --git a/TODO.md b/TODO.md index d7f8652..032e375 100644 --- a/TODO.md +++ b/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) diff --git a/app/public/admin/acces.php b/app/public/admin/acces.php index cd16dbc..83999a1 100644 --- a/app/public/admin/acces.php +++ b/app/public/admin/acces.php @@ -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(); diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 580103c..705395e 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -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

Des questions ou un problème avec le formulaire ? - xamxam@erg.be

+

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 diff --git a/app/src/Controllers/AboutController.php b/app/src/Controllers/AboutController.php index 63fec33..5b8eaec 100644 --- a/app/src/Controllers/AboutController.php +++ b/app/src/Controllers/AboutController.php @@ -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.", diff --git a/app/src/Controllers/LicenceController.php b/app/src/Controllers/LicenceController.php index 11dbf74..c1189e9 100644 --- a/app/src/Controllers/LicenceController.php +++ b/app/src/Controllers/LicenceController.php @@ -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, diff --git a/app/src/Dispatcher.php b/app/src/Dispatcher.php index a584ff1..80f420d 100644 --- a/app/src/Dispatcher.php +++ b/app/src/Dispatcher.php @@ -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); diff --git a/app/src/EmailObfuscator.php b/app/src/EmailObfuscator.php new file mode 100644 index 0000000..56220c3 --- /dev/null +++ b/app/src/EmailObfuscator.php @@ -0,0 +1,119 @@ +' . EmailObfuscator::email('name@domain.com') . ''; + */ + +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: + * foo@bar.com + * some text + * + * In the second case, only the href is obfuscated (the link text is kept). + */ + public static function obfuscateHtml(string $html): string + { + // Match tags whose href starts with mailto: + return preg_replace_callback( + '/]*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; + } +} diff --git a/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png b/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png deleted file mode 100644 index d9212f5..0000000 Binary files a/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png and /dev/null differ diff --git a/app/storage/covers/f2f1ef60698383a1e6a93dc7719dc5c3.png b/app/storage/covers/f2f1ef60698383a1e6a93dc7719dc5c3.png deleted file mode 100644 index a0824bd..0000000 Binary files a/app/storage/covers/f2f1ef60698383a1e6a93dc7719dc5c3.png and /dev/null differ diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index 38cf7c6..e30c5c9 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -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'])) : ''; ?> @@ -367,9 +380,7 @@
Email : - - - +
Date : diff --git a/app/templates/admin/file-access.php b/app/templates/admin/file-access.php index 8f4a364..c57a865 100644 --- a/app/templates/admin/file-access.php +++ b/app/templates/admin/file-access.php @@ -59,9 +59,7 @@
Email : - - - +
Date : diff --git a/app/templates/partials/form/form.php b/app/templates/partials/form/form.php index cec0b30..1ffb49b 100644 --- a/app/templates/partials/form/form.php +++ b/app/templates/partials/form/form.php @@ -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"], ); ?> @@ -127,6 +129,13 @@ $checkedFormatsForSiteWeb = $checkedFormatsForSiteWeb ?? []; ) ?>
+ + + +