diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b2ad0..ebd6242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` et `
` 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
 
 ### Ajouté
diff --git a/config/config.php b/config/config.php
index 2c6b447..4d6f9f5 100644
--- a/config/config.php
+++ b/config/config.php
@@ -44,3 +44,5 @@ if (!function_exists('url')) {
         return $u;
     }
 }
+
+require_once BASE_PATH . '/src/helpers.php';
diff --git a/public/login/index.php b/public/login/index.php
index 43c669a..51a680b 100644
--- a/public/login/index.php
+++ b/public/login/index.php
@@ -6,35 +6,6 @@ declare(strict_types=1);
 
 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')) {
     define('BASE_PATH', dirname(__DIR__, 2));
 }
diff --git a/public/login/magic.php b/public/login/magic.php
index e43135e..674c295 100644
--- a/public/login/magic.php
+++ b/public/login/magic.php
@@ -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) . '/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'] ?? '');
 if ($token === '' || preg_match('/[^A-Za-z0-9\-\_]/', $token)) {
     http_response_code(400);
diff --git a/public/oidc/callback.php b/public/oidc/callback.php
index 549a168..fb712b5 100644
--- a/public/oidc/callback.php
+++ b/public/oidc/callback.php
@@ -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) . '/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');
 
 $OIDC_ISSUER        = rtrim((string)(env('OIDC_ISSUER') ?? ''), '/');
diff --git a/public/oidc/start.php b/public/oidc/start.php
index ce1ca37..7f8c844 100644
--- a/public/oidc/start.php
+++ b/public/oidc/start.php
@@ -16,20 +16,6 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
     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'
 if (!in_array($flow, ['login','register'], true)) {
     $flow = 'login';
diff --git a/public/version.txt b/public/version.txt
index 9e7398a..d4ca915 100644
--- a/public/version.txt
+++ b/public/version.txt
@@ -1 +1 @@
-1.6.12
+1.6.13
diff --git a/src/Domain/User.php b/src/Domain/User.php
deleted file mode 100644
index da5e745..0000000
--- a/src/Domain/User.php
+++ /dev/null
@@ -1,16 +0,0 @@
-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 = <<pdo->prepare($sql);
-            $st->execute([
-                ':email' => $email,
-                ':hash'  => $randomHash,
-            ]);
-            return (string)$st->fetchColumn();
-        } catch (\PDOException $e) {
-            // Unique violation sur email (23505) → on relit l’id (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 = <<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;
-    }
-}
diff --git a/src/Service/AuthService.php b/src/Service/AuthService.php
deleted file mode 100644
index 4169fe7..0000000
--- a/src/Service/AuthService.php
+++ /dev/null
@@ -1,105 +0,0 @@
- 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 l’utilisateur (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 l’ancien 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();
-    }
-}
diff --git a/src/helpers.php b/src/helpers.php
index 96e10d8..e0867df 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -2,6 +2,27 @@
 
 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)
 {
     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%)";
 }
+
+/**
+ * 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 
 et  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[^>]*)>(.*?)#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;
+}
diff --git a/templates/post_view.php b/templates/post_view.php
index e47f181..2246962 100644
--- a/templates/post_view.php
+++ b/templates/post_view.php
@@ -35,6 +35,7 @@ $_renderedContent = preg_replace_callback(
     },
     $Parsedown->text($_rawForRender)
 );
+$_renderedContent = typographieHtml($_renderedContent ?? '');
 
 ob_start();