nettoyage & typo : dead code, helpers factorisés, guillemets courbes (v1.6.13)

- #19 : suppression AuthService / UserRepository / Domain\User — dead code incompatible session
- #22 : env() et db() centralisés dans src/helpers.php, chargé par config/config.php
- #15 : typographieHtml() appliquée après Parsedown dans post_view.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 23:36:09 +02:00
parent 88cc67d945
commit c17cad9c66
12 changed files with 81 additions and 325 deletions
+11
View File
@@ -5,6 +5,17 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
--- ---
## [1.6.13] - 2026-05-15
### Ajouté
- Typographie : guillemets droits convertis en guillemets courbes (`"``"` / `"`, `'``'` / `'`) dans le rendu des articles — blocs `<code>` et `<pre>` préservés (#15)
### Corrigé
- Suppression du dead code : `AuthService`, `UserRepository` et `Domain\User` — incompatibles avec le système de session actuel, aucune référence active (#19)
- Factorisation des helpers `env()` et `db()` dans `src/helpers.php`, chargé par `config/config.php` — plus de triple définition dans les pages login/OIDC (#22)
---
## [1.6.12] - 2026-05-15 ## [1.6.12] - 2026-05-15
### Ajouté ### Ajouté
+2
View File
@@ -44,3 +44,5 @@ if (!function_exists('url')) {
return $u; return $u;
} }
} }
require_once BASE_PATH . '/src/helpers.php';
-29
View File
@@ -6,35 +6,6 @@ declare(strict_types=1);
use App\Http\Csrf; use App\Http\Csrf;
// --- Helpers AVANT tout usage ---
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string
{
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
return (string)$_ENV[$key];
}
$v = getenv($key);
if ($v !== false && $v !== '') {
return (string)$v;
}
return $default;
}
}
if (!function_exists('db')) {
function db(): \PDO
{
return \App\Infrastructure\Database::get();
}
}
if (!function_exists('url')) {
function url(string $path = '/'): string
{
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
return $scheme . '://' . $host . $path;
}
}
if (!defined('BASE_PATH')) { if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2)); define('BASE_PATH', dirname(__DIR__, 2));
} }
-17
View File
@@ -12,23 +12,6 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__DIR__, 2) . '/config/config.php'; require_once dirname(__DIR__, 2) . '/config/config.php';
require_once dirname(__DIR__, 2) . '/bootstrap.php'; require_once dirname(__DIR__, 2) . '/bootstrap.php';
// si tu as un service pour ouvrir une session
if (!function_exists('db')) {
function db(): PDO
{
return \App\Infrastructure\Database::get();
}
}
if (!function_exists('url')) {
function url(string $path = '/'): string
{
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
return $scheme . '://' . $host . $path;
}
}
$token = (string)($_GET['token'] ?? ''); $token = (string)($_GET['token'] ?? '');
if ($token === '' || preg_match('/[^A-Za-z0-9\-\_]/', $token)) { if ($token === '' || preg_match('/[^A-Za-z0-9\-\_]/', $token)) {
http_response_code(400); http_response_code(400);
-14
View File
@@ -9,20 +9,6 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
require_once dirname(__DIR__, 2) . '/config/config.php'; require_once dirname(__DIR__, 2) . '/config/config.php';
require_once dirname(__DIR__, 2) . '/bootstrap.php'; require_once dirname(__DIR__, 2) . '/bootstrap.php';
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string
{
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
return (string)$_ENV[$key];
}
$v = getenv($key);
if ($v !== false && $v !== '') {
return (string)$v;
}
return $default;
}
}
$debug = (env('APP_DEBUG', '0') === '1'); $debug = (env('APP_DEBUG', '0') === '1');
$OIDC_ISSUER = rtrim((string)(env('OIDC_ISSUER') ?? ''), '/'); $OIDC_ISSUER = rtrim((string)(env('OIDC_ISSUER') ?? ''), '/');
-14
View File
@@ -16,20 +16,6 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
exit; exit;
} }
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string
{
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
return (string)$_ENV[$key];
}
$v = getenv($key);
if ($v !== false && $v !== '') {
return (string)$v;
}
return $default;
}
}
$flow = $_GET['flow'] ?? 'login'; // 'login' ou 'register' $flow = $_GET['flow'] ?? 'login'; // 'login' ou 'register'
if (!in_array($flow, ['login','register'], true)) { if (!in_array($flow, ['login','register'], true)) {
$flow = 'login'; $flow = 'login';
+1 -1
View File
@@ -1 +1 @@
1.6.12 1.6.13
-16
View File
@@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Domain;
final class User
{
public function __construct(
public string $id,
public string $email,
public string $passwordHash,
public bool $isActive = true,
) {
}
}
-129
View File
@@ -1,129 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Domain\User;
use PDO;
final class UserRepository
{
public function __construct(private PDO $pdo)
{
}
/**
* Crée (si besoin) un utilisateur OIDC.
* - Idempotent par email : si existe, retourne l'id existant.
* - Génère un password_hash aléatoire inutilisable (compte OIDC).
*
* @return string ID (uuid) sous forme de chaîne
*/
public function createFromOidc(string $email): string
{
$email = strtolower(trim($email));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email OIDC invalide.');
}
// 1) Existe déjà ?
$st = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
$st->execute([':email' => $email]);
$id = $st->fetchColumn();
if ($id !== false && $id !== null) {
return (string)$id;
}
// 2) Création
// Génère un hash robuste sur une valeur aléatoire (aucune chance de connexion par mot de passe).
$randomSecret = bin2hex(random_bytes(32));
$randomHash = password_hash($randomSecret, PASSWORD_DEFAULT);
$sql = <<<SQL
INSERT INTO users (email, password_hash)
VALUES (:email, :hash)
RETURNING id
SQL;
try {
$st = $this->pdo->prepare($sql);
$st->execute([
':email' => $email,
':hash' => $randomHash,
]);
return (string)$st->fetchColumn();
} catch (\PDOException $e) {
// Unique violation sur email (23505) → on relit lid (race condition)
if ($e->getCode() === '23505') {
$st = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
$st->execute([':email' => $email]);
$id = $st->fetchColumn();
if ($id !== false && $id !== null) {
return (string)$id;
}
}
throw $e;
}
}
public function findByEmail(string $email): ?User
{
$sql = 'SELECT id, email, password_hash, is_active FROM users WHERE email = :email LIMIT 1';
$st = $this->pdo->prepare($sql);
$st->execute([':email' => $email]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$isActive = $this->toBool($row['is_active']);
return new User(
(string)$row['id'],
(string)$row['email'],
(string)$row['password_hash'],
$isActive
);
}
public function create(string $email, string $passwordHash): string
{
// PostgreSQL
$sql = 'INSERT INTO users (email, password_hash) VALUES (:email, :hash) RETURNING id';
$st = $this->pdo->prepare($sql);
$st->execute([':email' => $email, ':hash' => $passwordHash]);
return (string)$st->fetchColumn();
}
public function updatePassword(string $userId, string $newHash): void
{
$sql = <<<SQL
UPDATE users
SET password_hash = :h,
updated_at = NOW(),
password_changed_at = NOW()
WHERE id = :id
SQL;
$st = $this->pdo->prepare($sql);
$st->execute([':h' => $newHash, ':id' => $userId]);
}
/**
* Normalise un bool venant de PDO/pgsql ('t','f',1,0,true,false,'1','0','true','false')
*/
private function toBool(mixed $v): bool
{
if (is_bool($v)) {
return $v;
}
if (is_int($v)) {
return $v === 1;
}
if (is_string($v)) {
$v = strtolower($v);
return in_array($v, ['t', '1', 'true', 'on', 'yes'], true);
}
return (bool)$v;
}
}
-105
View File
@@ -1,105 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepository;
final class AuthService
{
public function __construct(private UserRepository $users)
{
}
public function canAttempt(string $email, string $ip): bool
{
// backoff: 5 dernières tentatives/5 min
$sql = "select count(*)
from login_attempts
where ip = :ip
and attempted_at > now() - interval '5 minutes'
and success = false";
$st = \App\Infrastructure\Database::pdo()->prepare($sql);
$st->execute([':ip' => $ip]);
$fails = (int)$st->fetchColumn();
return $fails < 10; // à ajuster
}
public function login(string $email, string $password, string $ip): bool
{
$user = $this->users->findByEmail($email);
$ok = $user && $user->isActive && password_verify($password, $user->passwordHash);
$pdo = \App\Infrastructure\Database::pdo();
$st = $pdo->prepare('insert into login_attempts(email, ip, success) values(:e, :ip, :s)');
$st->bindValue(':e', $email, \PDO::PARAM_STR);
$st->bindValue(':ip', $ip, \PDO::PARAM_STR);
$st->bindValue(':s', $ok, \PDO::PARAM_BOOL);
$st->execute();
if ($ok) {
\App\Infrastructure\Session::regenerate();
$_SESSION['uid'] = $user->id;
$_SESSION['email'] = $user->email;
}
return $ok;
}
public function changePassword(string $userId, string $currentPassword, string $newPassword): bool
{
// Récupération de lutilisateur (rapide : requête directe ; tu peux créer findById() si tu préfères)
$pdo = \App\Infrastructure\Database::pdo();
$st = $pdo->prepare('select id, email, password_hash, is_active from users where id = :id');
$st->execute([':id' => $userId]);
$row = $st->fetch(\PDO::FETCH_ASSOC);
if (!$row || !(bool)$row['is_active']) {
return false;
}
// Vérifier lancien mot de passe
if (!password_verify($currentPassword, (string)$row['password_hash'])) {
return false;
}
// Politique minimale : longueur uniquement (espaces autorisés)
if (mb_strlen($newPassword) < 7) {
return false;
}
// (optionnel) interdire seulement le caractère NUL
if (strpos($newPassword, "\0") !== false) {
return false;
}
// Mettre à jour le hash
$newHash = password_hash($newPassword, PASSWORD_ARGON2ID);
(new \App\Repository\UserRepository(\App\Infrastructure\Database::get()))->updatePassword($row['id'], $newHash);
// (Optionnel) rotation session
\App\Infrastructure\Session::regenerate();
return true;
}
public function register(string $email, string $password): string
{
$hash = password_hash($password, PASSWORD_ARGON2ID);
return $this->users->create($email, $hash);
}
public static function requireAuth(): void
{
if (!isset($_SESSION['uid'])) {
header('Location: /login');
exit;
}
}
public static function logout(): void
{
$_SESSION = [];
session_destroy();
}
}
+66
View File
@@ -2,6 +2,27 @@
declare(strict_types=1); declare(strict_types=1);
if (!function_exists('env')) {
function env(string $key, ?string $default = null): ?string
{
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
return (string)$_ENV[$key];
}
$v = getenv($key);
if ($v !== false && $v !== '') {
return (string)$v;
}
return $default;
}
}
if (!function_exists('db')) {
function db(): \PDO
{
return \App\Infrastructure\Database::get();
}
}
function vd($var, ...$moreVars) function vd($var, ...$moreVars)
{ {
ob_start(); ob_start();
@@ -149,3 +170,48 @@ function _paletteGradient(array $rgb, int $tier): string
return "linear-gradient({$angle}deg,rgb($tr,$tg,$tb) 0%,rgb($sr,$sg,$sb) 100%)"; return "linear-gradient({$angle}deg,rgb($tr,$tg,$tb) 0%,rgb($sr,$sg,$sb) 100%)";
} }
/**
* Post-traite le HTML produit par Parsedown pour y appliquer la typographie française :
* guillemets droits guillemets courbes, apostrophes droites apostrophes courbes.
* Le contenu des balises <pre> et <code> est strictement préservé.
*/
function typographieHtml(string $html): string
{
// Protéger les blocs pre/code (y compris imbriqués)
$protected = [];
$html = preg_replace_callback(
'#<(pre|code)(\b[^>]*)>(.*?)</\1>#si',
static function (array $m) use (&$protected): string {
$key = "\x02" . count($protected) . "\x03";
$protected[$key] = $m[0];
return $key;
},
$html
) ?? $html;
// Traiter uniquement les nœuds texte (entre les balises HTML)
$html = preg_replace_callback(
'#(<[^>]+>)|([^<]+)#s',
static function (array $m): string {
if ($m[1] !== '') {
return $m[1]; // balise HTML — intacte
}
$t = $m[2];
// Guillemets doubles : précédé d'un mot → fermant, sinon → ouvrant
$t = preg_replace('/(?<=\w)"/u', "\u{201D}", $t);
$t = str_replace('"', "\u{201C}", $t);
// Apostrophes / guillemets simples : précédé d'un mot → fermant/apostrophe, sinon → ouvrant
$t = preg_replace("/(?<=\w)'/u", "\u{2019}", $t);
$t = str_replace("'", "\u{2018}", $t);
return $t;
},
$html
) ?? $html;
// Restaurer les blocs protégés
if ($protected) {
$html = str_replace(array_keys($protected), array_values($protected), $html);
}
return $html;
}
+1
View File
@@ -35,6 +35,7 @@ $_renderedContent = preg_replace_callback(
}, },
$Parsedown->text($_rawForRender) $Parsedown->text($_rawForRender)
); );
$_renderedContent = typographieHtml($_renderedContent ?? '');
ob_start(); ob_start();