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:
@@ -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
|
||||
|
||||
### Ajouté
|
||||
|
||||
@@ -44,3 +44,5 @@ if (!function_exists('url')) {
|
||||
return $u;
|
||||
}
|
||||
}
|
||||
|
||||
require_once BASE_PATH . '/src/helpers.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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') ?? ''), '/');
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.6.12
|
||||
1.6.13
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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 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 = <<<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;
|
||||
}
|
||||
}
|
||||
@@ -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 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <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;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ $_renderedContent = preg_replace_callback(
|
||||
},
|
||||
$Parsedown->text($_rawForRender)
|
||||
);
|
||||
$_renderedContent = typographieHtml($_renderedContent ?? '');
|
||||
|
||||
ob_start();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user