Initial commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once BASE_PATH . '/src/db.php';
|
||||
require_once BASE_PATH . '/src/PostManager.php';
|
||||
|
||||
$postManager = new PostManager($db);
|
||||
|
||||
$errors = [];
|
||||
$title = '';
|
||||
$content = '';
|
||||
$published = false;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = trim($_POST['content'] ?? '');
|
||||
$published = isset($_POST['published']);
|
||||
|
||||
if ($title === '') {
|
||||
$errors[] = 'Le titre est obligatoire.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$postId = $postManager->create($title, $content, $published);
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nouveau post</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container py-4">
|
||||
<h1 class="mb-4">Ajouter un nouveau post</h1>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?= htmlspecialchars($error) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Titre</label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="<?= htmlspecialchars($title) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">Contenu</label>
|
||||
<textarea class="form-control" id="content" name="content" rows="6"><?= htmlspecialchars($content) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="published" name="published" <?= $published ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="published">Publier immédiatement</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
<a href="index.php" class="btn btn-secondary">Annuler</a>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
||||
|
||||
require_once BASE_PATH . '/src/helpers.php';
|
||||
require_once BASE_PATH . '/config/config.php';
|
||||
require_once BASE_PATH . '/src/db.php';
|
||||
require_once BASE_PATH . '/src/PostManager.php';
|
||||
|
||||
$postManager = new PostManager($db);
|
||||
|
||||
ob_start();
|
||||
|
||||
$posts = $postManager->getAll();
|
||||
require_once BASE_PATH . '/templates/post_list.php';
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
// public/login/config.php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
if (!defined('BASE_PATH')) { require_once dirname(__DIR__, 2) . '/config/config.php'; }
|
||||
require_once BASE_PATH . '/includes/db.php';
|
||||
require_once BASE_PATH . '/includes/csrf.php';
|
||||
require_once BASE_PATH . '/includes/ConfigRepo.php';
|
||||
|
||||
Session::startSecure(getenv('SESSION_NAME') ?: 'SID_IDENT');
|
||||
ensure_admin();
|
||||
csrf_start();
|
||||
|
||||
$cfg = config_repo_get();
|
||||
$msg = null; $err = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!csrf_check($_POST['csrf'] ?? '')) { http_response_code(403); exit('CSRF'); }
|
||||
|
||||
$in = [
|
||||
'oidc_issuer' => trim((string)($_POST['oidc_issuer'] ?? '')),
|
||||
'oidc_name' => trim((string)($_POST['oidc_name'] ?? '')),
|
||||
'oidc_client_id' => trim((string)($_POST['oidc_client_id'] ?? '')),
|
||||
'oidc_client_secret'=> trim((string)($_POST['oidc_client_secret'] ?? '')),
|
||||
'oidc_redirect_uri' => trim((string)($_POST['oidc_redirect_uri'] ?? '')),
|
||||
];
|
||||
|
||||
// validations simples
|
||||
if ($in['allow_oidc']) {
|
||||
if ($in['oidc_issuer'] === '' || $in['oidc_client_id'] === '' || $in['oidc_client_secret'] === '' || $in['oidc_redirect_uri'] === '') {
|
||||
$err = "OIDC activé mais champs incomplets.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!$err) {
|
||||
config_repo_save($in);
|
||||
|
||||
// Mise à jour du .env
|
||||
$envPairs = [
|
||||
'OIDC_ISSUER' => $in['oidc_issuer'] !== '' ? $in['oidc_issuer'] : null,
|
||||
'OIDC_NAME' => $in['oidc_name'] !== '' ? $in['oidc_name'] : null,
|
||||
'OIDC_CLIENT_ID' => $in['oidc_client_id'] !== '' ? $in['oidc_client_id'] : null,
|
||||
'OIDC_CLIENT_SECRET' => $in['oidc_client_secret'] !== '' ? $in['oidc_client_secret'] : null,
|
||||
'OIDC_REDIRECT_URI' => $in['oidc_redirect_uri'] !== '' ? $in['oidc_redirect_uri'] : null,
|
||||
];
|
||||
env_set_pairs(BASE_PATH.'/.env', $envPairs);
|
||||
|
||||
$cfg = config_repo_get();
|
||||
$msg = "Configuration enregistrée.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Configuration authentification</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link href="/assets/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container py-4">
|
||||
<h1 class="h3 mb-3">Configuration authentification</h1>
|
||||
|
||||
<?php if ($msg): ?><div class="alert alert-success"><?=htmlspecialchars($msg)?></div><?php endif; ?>
|
||||
<?php if ($err): ?><div class="alert alert-danger"><?=htmlspecialchars($err)?></div><?php endif; ?>
|
||||
|
||||
<form method="post" class="card p-3">
|
||||
<input type="hidden" name="csrf" value="<?=htmlspecialchars(csrf_token())?>">
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h5">Modes de connexion</legend>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="allow_password" name="allow_password" <?= $cfg['allow_password'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="allow_password">Login + mot de passe autorisé</label>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="allow_oidc" name="allow_oidc" <?= $cfg['allow_oidc'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="allow_oidc">Connexion OIDC autorisée</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h5">Inscriptions</legend>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" id="reg_open" name="registrations_open" value="open" <?= $cfg['registrations_open'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="reg_open">Ouvertes à tous</label>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="radio" id="reg_closed" name="registrations_open" value="closed" <?= !$cfg['registrations_open'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="reg_closed">Fermées</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="mb-3">
|
||||
<legend class="h5">Paramètres OIDC</legend>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Issuer URL</label>
|
||||
<input type="url" name="oidc_issuer" class="form-control" value="<?=htmlspecialchars((string)$cfg['oidc_issuer'])?>" placeholder="https://idp.example.com/realms/xxx">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nom affiché</label>
|
||||
<input type="text" name="oidc_name" class="form-control" value="<?=htmlspecialchars((string)$cfg['oidc_name'])?>" placeholder="Keycloak, Azure AD…">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client ID</label>
|
||||
<input type="text" name="oidc_client_id" class="form-control" value="<?=htmlspecialchars((string)$cfg['oidc_client_id'])?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client Secret</label>
|
||||
<input type="password" name="oidc_client_secret" class="form-control" value="<?=htmlspecialchars((string)$cfg['oidc_client_secret'])?>">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Redirect URI</label>
|
||||
<input type="url" name="oidc_redirect_uri" class="form-control" value="<?=htmlspecialchars((string)$cfg['oidc_redirect_uri'])?>" placeholder="<?=htmlspecialchars(rtrim(getenv('APP_URL') ?: '', '/').'/oidc/callback')?>">
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-text mt-2">Ces champs alimentent le fichier <code>.env</code>.</p>
|
||||
</fieldset>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary" type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
// projet : mug.a5l.fr
|
||||
// fichier : pages/login/index.php
|
||||
// version : 20251011
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||
|
||||
// Paramètres (env)
|
||||
$ttlMin = (int) env('MAGIC_LINK_TTL_MINUTES', '30');
|
||||
$coolMin = (int) env('MAGIC_COOLDOWN_MINUTES', '5');
|
||||
$winHours = (int) env('MAGIC_WINDOW_HOURS', '12');
|
||||
$maxPerWin = (int) env('MAGIC_MAX_PER_WINDOW', '5');
|
||||
|
||||
// --- return_to ---
|
||||
$defaultReturn = '/';
|
||||
$sanitize = static function (string $url) use ($defaultReturn): string {
|
||||
$url = trim($url);
|
||||
if ($url === '' || !str_starts_with($url, '/')) return $defaultReturn;
|
||||
return $url;
|
||||
};
|
||||
$returnTo = $sanitize((string)($_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?? $defaultReturn)));
|
||||
|
||||
// --- OIDC ---
|
||||
$oidcEnabled = (bool) (env('OIDC_ISSUER') && env('OIDC_CLIENT_ID'));
|
||||
$oidcLoginUrl = '/login/oidc' . ($returnTo ? ('?return_to=' . urlencode($returnTo)) : '');
|
||||
$oidcAuto = (isset($_GET['sso']) && $_GET['sso'] === '1') || (env('OIDC_AUTO', '0') === '1');
|
||||
if ($oidcEnabled && $oidcAuto) { header('Location: ' . $oidcLoginUrl, true, 302); exit; }
|
||||
|
||||
// --- form: demande de lien magique ---
|
||||
$errors = [];
|
||||
$okMsg = '';
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
if (!Csrf::validate($_POST['_csrf'] ?? null)) {
|
||||
http_response_code(400);
|
||||
$errors[] = 'Jeton CSRF invalide.';
|
||||
} else {
|
||||
$email = strtolower(trim((string)($_POST['email'] ?? '')));
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = 'Adresse email invalide.';
|
||||
} else {
|
||||
// rate limit simple par email et IP
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||
if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip, 2)[0]);
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// purge expirés / consommés
|
||||
$pdo->prepare("DELETE FROM auth_magic_links WHERE email = :e AND (expires_at < NOW() OR consumed_at IS NOT NULL)")
|
||||
->execute([':e' => $email]);
|
||||
|
||||
// 1) cooldown: refuser si un envoi récent < coolMin
|
||||
$sql = sprintf(
|
||||
"SELECT 1 FROM auth_magic_links
|
||||
WHERE email = :e AND created_at >= NOW() - INTERVAL '%d minutes'
|
||||
LIMIT 1",
|
||||
max(0, $coolMin)
|
||||
);
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([':e' => $email]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
throw new RuntimeException(sprintf('Un lien vient d’être envoyé. Réessayez dans %d min.
|
||||
Si vous ne recevez toujours rien, envisagez d\'utiliser un fournisseur de messagerie respectueux de la vie privée,
|
||||
comme Proton Mail, Tuta, Posteo, Mailfence ou Infomaniak, qui garantissent un hébergement européen
|
||||
et ne revendent pas vos données. -- Cédrix, le 11/10/2025', $coolMin));
|
||||
}
|
||||
|
||||
// 2) plafond: maxPerWin liens sur winHours
|
||||
$sql = sprintf(
|
||||
"SELECT COUNT(*) FROM auth_magic_links
|
||||
WHERE email = :e AND created_at >= NOW() - INTERVAL '%d hours'",
|
||||
max(0, $winHours)
|
||||
);
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([':e' => $email]);
|
||||
if ((int)$stmt->fetchColumn() >= $maxPerWin) {
|
||||
throw new RuntimeException('Quota atteint. Réessayez plus tard.');
|
||||
}
|
||||
|
||||
// Génère et enregistre le lien avec TTL ttlMin
|
||||
$raw = random_bytes(32);
|
||||
$token = rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO auth_magic_links (id,email,token,created_at,expires_at,ip,user_agent,return_to)
|
||||
VALUES (gen_random_uuid(), :email, :token, NOW(), NOW() + INTERVAL '%d minutes', :ip, :ua, :rt)
|
||||
RETURNING token",
|
||||
max(1, $ttlMin)
|
||||
);
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':email' => $email,
|
||||
':token' => $token,
|
||||
':ip' => $ip,
|
||||
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512),
|
||||
':rt' => ($returnTo !== '/' ? $returnTo : null),
|
||||
]);
|
||||
$pdo->commit();
|
||||
|
||||
// construit l’URL et ENVOIE le mail ici...
|
||||
$magicUrl = url('/login/magic.php') . '?token=' . urlencode($token);
|
||||
/* envoyer_mail_smtp(...) ou mail(...) */
|
||||
|
||||
// message utilisateur
|
||||
$okMsg = "Un lien vient d'être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.";
|
||||
|
||||
} catch (\Throwable $ex) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
$errors[] = $ex->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$csrf = Csrf::token();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Connexion</title>
|
||||
<link rel="stylesheet" href="/assets/bootstrap/bootstrap.min.css">
|
||||
<style>.or-sep{display:flex;align-items:center;gap:.75rem;margin:1.25rem 0}.or-sep::before,.or-sep::after{content:"";flex:1;height:1px;background:#ddd}</style>
|
||||
</head>
|
||||
<body class="container py-5">
|
||||
<h1 class="mb-3">Bienvenue 👋</h1>
|
||||
<p class="text-muted">Vous n’êtes pas connecté. Accédez auxfonctionnalités en vous identifiant.</p>
|
||||
|
||||
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($e, ENT_QUOTES) ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($okMsg): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($okMsg, ENT_QUOTES) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($oidcEnabled): ?>
|
||||
<div class="mb-3">
|
||||
<a class="btn btn-primary w-100" href="<?= htmlspecialchars($oidcLoginUrl, ENT_QUOTES) ?>">Se connecter avec A5L</a>
|
||||
</div>
|
||||
<div class="or-sep"><span>ou</span></div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">A5L indisponible : configurez <code>OIDC_ISSUER</code> et <code>OIDC_CLIENT_ID</code> dans <code>.env</code>.</div>
|
||||
<div class="or-sep"><span>ou</span></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/login<?= $returnTo ? ('?return_to=' . urlencode($returnTo)) : '' ?>" novalidate>
|
||||
<input type="hidden" name="_csrf" value="<?= htmlspecialchars($csrf, ENT_QUOTES) ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email">Adresse email</label>
|
||||
<input class="form-control" id="email" type="email" name="email" required autocomplete="email" inputmode="email" placeholder="vous@domaine.tld">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Recevoir un lien magique</button>
|
||||
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
// projet : mug.a5l.fr
|
||||
// fichier : pages/login/magic.php
|
||||
// version : 20251011
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||
|
||||
use App\Service\AuthService; // 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);
|
||||
exit('Lien invalide.');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// récupère lien non consommé et non expiré
|
||||
$sql = "SELECT id, email, token, created_at, expires_at, consumed_at, return_to
|
||||
FROM auth_magic_links
|
||||
WHERE token = :t
|
||||
FOR UPDATE";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([':t' => $token]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) throw new RuntimeException('Lien inconnu.');
|
||||
if ($row['consumed_at'] !== null) throw new RuntimeException('Lien déjà utilisé.');
|
||||
if (strtotime((string)$row['expires_at']) < time()) throw new RuntimeException('Lien expiré.');
|
||||
|
||||
// consomme le lien
|
||||
$pdo->prepare("UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id")->execute([':id' => $row['id']]);
|
||||
$pdo->commit();
|
||||
|
||||
// ouvre une session applicative « anonyme authentifiée par email »
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
$_SESSION['auth'] = [
|
||||
'method' => 'magic',
|
||||
'email' => (string)$row['email'],
|
||||
'ts' => time(),
|
||||
];
|
||||
// Aucun create user ici, conforme à la demande
|
||||
|
||||
$dest = $row['return_to'] ?? '/';
|
||||
// sécurité: ne renvoyer que des chemins relatifs
|
||||
if (!is_string($dest) || !str_starts_with($dest, '/')) $dest = '/';
|
||||
header('Location: ' . $dest, true, 303);
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
http_response_code(400);
|
||||
echo htmlspecialchars($e->getMessage(), ENT_QUOTES);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
// proxy vers pages/oidc/start.php avec flow=login
|
||||
$_GET['flow'] = 'login';
|
||||
require_once dirname(__DIR__) . '/oidc/start.php';
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Infrastructure\Database;
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
require_once dirname(__DIR__, 2) . '/config/config.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 config ---
|
||||
$OIDC_ISSUER = rtrim((string)env('OIDC_ISSUER', ''), '/');
|
||||
$OIDC_CLIENT_ID = (string)env('OIDC_CLIENT_ID', '');
|
||||
$OIDC_CLIENT_SECRET = (string)env('OIDC_CLIENT_SECRET', '');
|
||||
$OIDC_REDIRECT_URI = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback'));
|
||||
|
||||
if (!$OIDC_ISSUER || !$OIDC_CLIENT_ID || !$OIDC_REDIRECT_URI) {
|
||||
http_response_code(500);
|
||||
echo $debug ? 'OIDC config manquante.' : 'Erreur.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$tokenEndpoint = $OIDC_ISSUER . '/protocol/openid-connect/token';
|
||||
$userInfoEndpoint = $OIDC_ISSUER . '/protocol/openid-connect/userinfo';
|
||||
|
||||
// --- Checks retour ---
|
||||
if (!isset($_GET['state'], $_SESSION['oidc_state']) || !hash_equals((string)$_SESSION['oidc_state'], (string)$_GET['state'])) {
|
||||
http_response_code(400);
|
||||
echo $debug ? 'State invalide.' : 'Requête invalide.';
|
||||
exit;
|
||||
}
|
||||
unset($_SESSION['oidc_state']);
|
||||
|
||||
if (empty($_GET['code'])) {
|
||||
http_response_code(400);
|
||||
echo $debug ? 'Code manquant.' : 'Requête invalide.';
|
||||
exit;
|
||||
}
|
||||
$code = (string)$_GET['code'];
|
||||
|
||||
$codeVerifier = $_SESSION['oidc_code_verifier'] ?? null;
|
||||
unset($_SESSION['oidc_code_verifier']); // anti-replay
|
||||
$expectedNonce = $_SESSION['oidc_nonce'] ?? null;
|
||||
unset($_SESSION['oidc_nonce']); // anti-replay
|
||||
|
||||
if (!$codeVerifier) {
|
||||
http_response_code(400);
|
||||
echo $debug ? 'PKCE code_verifier manquant.' : 'Requête invalide.';
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Échange code -> tokens ---
|
||||
$post = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $code,
|
||||
'redirect_uri' => $OIDC_REDIRECT_URI,
|
||||
'client_id' => $OIDC_CLIENT_ID,
|
||||
'code_verifier' => $codeVerifier,
|
||||
];
|
||||
if ($OIDC_CLIENT_SECRET !== '') $post['client_secret'] = $OIDC_CLIENT_SECRET;
|
||||
|
||||
$ch = curl_init($tokenEndpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($post, '', '&', PHP_QUERY_RFC3986),
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$tokenResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($tokenResponse === false || $httpCode !== 200) {
|
||||
http_response_code(500);
|
||||
echo $debug ? 'Échec échange token: ' . htmlspecialchars($err ?: (string)$tokenResponse) : 'Erreur d’authentification.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$tokens = json_decode((string)$tokenResponse, true) ?: [];
|
||||
$accessToken = $tokens['access_token'] ?? null;
|
||||
$idToken = $tokens['id_token'] ?? null;
|
||||
|
||||
if (!$accessToken) {
|
||||
http_response_code(500);
|
||||
echo $debug ? 'Access token manquant.' : 'Erreur d’authentification.';
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- UserInfo ---
|
||||
$ch = curl_init($userInfoEndpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
$userInfoResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($userInfoResponse === false || $httpCode !== 200) {
|
||||
http_response_code(500);
|
||||
echo $debug ? 'Échec UserInfo: ' . htmlspecialchars((string)$userInfoResponse) : 'Erreur d’authentification.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$claims = json_decode((string)$userInfoResponse, true) ?: [];
|
||||
|
||||
// --- Récup info utiles ---
|
||||
$email = $claims['email'] ?? null;
|
||||
$username = $claims['preferred_username'] ?? ($email ?: null);
|
||||
$firstname = $claims['given_name'] ?? null;
|
||||
$lastname = $claims['family_name'] ?? null;
|
||||
|
||||
if (!$email && $idToken && substr_count($idToken, '.') === 2) {
|
||||
[, $p, ] = explode('.', $idToken, 3);
|
||||
$payloadJson = base64_decode(strtr($p, '-_', '+/'), true);
|
||||
$payload = $payloadJson ? json_decode($payloadJson, true) : null;
|
||||
if (is_array($payload) && !empty($payload['email'])) $email = $payload['email'];
|
||||
}
|
||||
|
||||
if (!$email) {
|
||||
http_response_code(400);
|
||||
echo $debug ? 'Email non fourni par IdP.' : 'Impossible de récupérer votre email.';
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Si l'utilisateur existe déjà -> connecter et redirect ---
|
||||
$flow = $_SESSION['oidc_flow'] ?? 'login';
|
||||
|
||||
// Vérifie existence en base
|
||||
/** @var \PDO $pdo */
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
|
||||
$stmt->execute([':email' => $email]);
|
||||
$existingId = $stmt->fetchColumn();
|
||||
|
||||
// Si flow=login ET utilisateur existe → connexion directe
|
||||
if ($flow === 'login' && $existingId) {
|
||||
$_SESSION['user_id'] = (int)$existingId;
|
||||
$_SESSION['user_email'] = $email;
|
||||
$_SESSION['oidc'] = [
|
||||
'issuer' => $OIDC_ISSUER,
|
||||
'sub' => $claims['sub'] ?? null,
|
||||
'access_token' => $accessToken,
|
||||
'id_token' => $idToken,
|
||||
'expires_at' => time() + (int)($tokens['expires_in'] ?? 3600),
|
||||
];
|
||||
$target = $_SESSION['oidc_return_to'] ?? '/';
|
||||
unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']);
|
||||
if (!is_string($target) || $target === '' || $target[0] !== '/') $target = '/';
|
||||
header('Location: ' . $target, true, 303);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Sinon : go formulaire d’inscription (pré-rempli)
|
||||
$_SESSION['pending_oidc'] = [
|
||||
'issuer' => $OIDC_ISSUER,
|
||||
'sub' => $claims['sub'] ?? null,
|
||||
'email' => $email,
|
||||
'username' => $claims['preferred_username'] ?? ($email ?: null),
|
||||
'firstname' => $claims['given_name'] ?? null,
|
||||
'lastname' => $claims['family_name'] ?? null,
|
||||
];
|
||||
unset($_SESSION['oidc_flow']);
|
||||
|
||||
header('Location: ' . url('register/from-oidc'), true, 303);
|
||||
exit;
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
// projet : mug.a5l.fr
|
||||
// fichier : pages/oidc/me.php
|
||||
// version : 20251005
|
||||
declare(strict_types=1);
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||
|
||||
function maskToken(?string $t): string {
|
||||
if (!$t) return '';
|
||||
$len = strlen($t);
|
||||
if ($len <= 12) return str_repeat('•', $len);
|
||||
return substr($t, 0, 6) . str_repeat('•', max(0, $len - 12)) . substr($t, -6);
|
||||
}
|
||||
function b64url_decode_str(string $s): string|false {
|
||||
$s = strtr($s, '-_', '+/');
|
||||
$pad = strlen($s) % 4;
|
||||
if ($pad) $s .= str_repeat('=', 4 - $pad);
|
||||
return base64_decode($s, true);
|
||||
}
|
||||
function decode_jwt(string $jwt): array {
|
||||
if (substr_count($jwt, '.') !== 2) return [];
|
||||
[, $payload, ] = explode('.', $jwt, 3);
|
||||
$json = b64url_decode_str($payload);
|
||||
if ($json === false) return [];
|
||||
$arr = json_decode($json, true);
|
||||
return is_array($arr) ? $arr : [];
|
||||
}
|
||||
|
||||
$env = static function(string $k, ?string $d = null): ?string {
|
||||
if (array_key_exists($k, $_ENV) && $_ENV[$k] !== '') return (string)$_ENV[$k];
|
||||
$v = getenv($k);
|
||||
if ($v !== false && $v !== '') return (string)$v;
|
||||
return $d;
|
||||
};
|
||||
|
||||
$debugEnabled = ($env('DEBUG_OIDC') === 'true') || (isset($_GET['debug']) && $_GET['debug'] === '1');
|
||||
|
||||
$oidc = $_SESSION['oidc'] ?? [];
|
||||
$claims = $_SESSION['oidc_userinfo'] ?? [];
|
||||
$issuer = (string)($oidc['issuer'] ?? '');
|
||||
$sub = (string)($oidc['sub'] ?? '');
|
||||
$idToken = (string)($oidc['id_token'] ?? '');
|
||||
$accTok = (string)($oidc['access_token'] ?? '');
|
||||
$expAt = (int) ($oidc['expires_at'] ?? 0);
|
||||
|
||||
$now = time();
|
||||
$left = $expAt ? max(0, $expAt - $now) : null;
|
||||
|
||||
// Fallback 1 : si pas de claims userinfo, essayer de les lire dans l'id_token
|
||||
if (!$claims && $idToken) {
|
||||
$claims = decode_jwt($idToken);
|
||||
}
|
||||
|
||||
// Fallback 2 (debug) : tenter un appel live au UserInfo si access_token présent
|
||||
if ($debugEnabled && $claims === [] && $accTok && $issuer) {
|
||||
$userinfoEndpoint = rtrim($issuer, '/') . '/protocol/openid-connect/userinfo';
|
||||
$ch = curl_init($userinfoEndpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accTok],
|
||||
CURLOPT_TIMEOUT => 6,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($resp !== false && $code === 200) {
|
||||
$tmp = json_decode((string)$resp, true);
|
||||
if (is_array($tmp)) $claims = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
// Extraire rôles groupés (Keycloak)
|
||||
$roles = [];
|
||||
if (isset($claims['realm_access']['roles']) && is_array($claims['realm_access']['roles'])) {
|
||||
$roles = array_merge($roles, $claims['realm_access']['roles']);
|
||||
}
|
||||
if (isset($claims['resource_access']) && is_array($claims['resource_access'])) {
|
||||
foreach ($claims['resource_access'] as $clientId => $data) {
|
||||
if (!empty($data['roles']) && is_array($data['roles'])) {
|
||||
foreach ($data['roles'] as $r) {
|
||||
$roles[] = $clientId . ':' . $r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$roles = array_values(array_unique($roles));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OIDC • Profil</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/assets/bootstrap/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.kv { display:grid; grid-template-columns: 220px 1fr; gap:.5rem 1rem; }
|
||||
.kv dt { font-weight: 600; color: #555; }
|
||||
pre { background: #f8f9fa; padding: .75rem; border-radius: .5rem; overflow:auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="container py-4">
|
||||
<h1 class="mb-4">Profil A5L</h1>
|
||||
|
||||
<?php if (!$oidc): ?>
|
||||
<div class="alert alert-warning">Aucune session A5L. Connecte-toi via A5L d'abord.</div>
|
||||
<?php else: ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Session / Jetons</div>
|
||||
<div class="card-body">
|
||||
<dl class="kv">
|
||||
<dt>Issuer</dt><dd><?= htmlspecialchars($issuer) ?></dd>
|
||||
<dt>Subject (sub)</dt><dd><?= htmlspecialchars($sub) ?></dd>
|
||||
<dt>ID Token</dt><dd><code><?= htmlspecialchars(maskToken($idToken)) ?></code></dd>
|
||||
<dt>Access Token</dt><dd><code><?= htmlspecialchars(maskToken($accTok)) ?></code></dd>
|
||||
<dt>Expire à</dt><dd><?= $expAt ? date('Y-m-d H:i:s', $expAt) : '—' ?></dd>
|
||||
<dt>Temps restant</dt><dd><?= $left !== null ? ($left . ' s') : '—' ?></dd>
|
||||
</dl>
|
||||
<?php if ($debugEnabled): ?>
|
||||
<details class="mt-3">
|
||||
<summary>Voir jetons non masqués (danger)</summary>
|
||||
<div class="mt-2">
|
||||
<div><strong>ID Token</strong></div>
|
||||
<pre><?= htmlspecialchars($idToken) ?></pre>
|
||||
<div><strong>Access Token</strong></div>
|
||||
<pre><?= htmlspecialchars($accTok) ?></pre>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Claims</div>
|
||||
<div class="card-body">
|
||||
<dl class="kv">
|
||||
<dt>Email</dt><dd><?= htmlspecialchars((string)($claims['email'] ?? '')) ?></dd>
|
||||
<dt>Preferred username</dt><dd><?= htmlspecialchars((string)($claims['preferred_username'] ?? '')) ?></dd>
|
||||
<dt>Given name</dt><dd><?= htmlspecialchars((string)($claims['given_name'] ?? '')) ?></dd>
|
||||
<dt>Family name</dt><dd><?= htmlspecialchars((string)($claims['family_name'] ?? '')) ?></dd>
|
||||
<dt>Name</dt><dd><?= htmlspecialchars((string)($claims['name'] ?? '')) ?></dd>
|
||||
<dt>Locale</dt><dd><?= htmlspecialchars((string)($claims['locale'] ?? '')) ?></dd>
|
||||
<dt>Rôles</dt>
|
||||
<dd>
|
||||
<?php if ($roles): ?>
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($roles as $r): ?>
|
||||
<li><?= htmlspecialchars((string)$r) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<?php if ($debugEnabled): ?>
|
||||
<h6 class="mt-3">Claims (JSON complet)</h6>
|
||||
<pre><?= htmlspecialchars(json_encode($claims, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?></pre>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$claims): ?>
|
||||
<div class="alert alert-info mt-3">
|
||||
Aucun claim reçu. Vérifie que ton <code>callback</code> remplit bien <code>$_SESSION['oidc_userinfo']</code> ou que l’<code>ID Token</code> contient les champs.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a class="btn btn-secondary" href="<?= htmlspecialchars(url('')) ?>">Retour</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||
require_once dirname(__DIR__, 2) . '/config/config.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;
|
||||
}
|
||||
}
|
||||
|
||||
$flow = $_GET['flow'] ?? 'login'; // 'login' ou 'register'
|
||||
if (!in_array($flow, ['login','register'], true)) $flow = 'login';
|
||||
|
||||
// return_to (URL relative uniquement)
|
||||
$defaultReturn = '/';
|
||||
$rawReturn = $_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?? $defaultReturn);
|
||||
$returnTo = (is_string($rawReturn) && str_starts_with($rawReturn, '/')) ? $rawReturn : $defaultReturn;
|
||||
|
||||
// Mémorise flow + cible
|
||||
$_SESSION['oidc_flow'] = $flow;
|
||||
$_SESSION['oidc_return_to'] = $returnTo;
|
||||
|
||||
// --- OIDC conf ---
|
||||
$issuer = rtrim((string)env('OIDC_ISSUER',''), '/');
|
||||
$clientId = (string)env('OIDC_CLIENT_ID','');
|
||||
$redirectUri = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback'));
|
||||
if (!$issuer || !$clientId || !$redirectUri) {
|
||||
http_response_code(500);
|
||||
echo 'OIDC non configuré (OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_REDIRECT_URI).';
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Endpoints & PKCE ---
|
||||
$authEndpoint = $issuer . '/protocol/openid-connect/auth';
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
$codeVerifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
|
||||
$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');
|
||||
|
||||
$_SESSION['oidc_state'] = $state;
|
||||
$_SESSION['oidc_nonce'] = $nonce;
|
||||
$_SESSION['oidc_code_verifier'] = $codeVerifier;
|
||||
|
||||
// --- URL d’auth ---
|
||||
$params = [
|
||||
'response_type' => 'code',
|
||||
'client_id' => $clientId,
|
||||
'redirect_uri' => $redirectUri,
|
||||
'scope' => 'openid email profile',
|
||||
'state' => $state,
|
||||
'nonce' => $nonce,
|
||||
'code_challenge' => $codeChallenge,
|
||||
'code_challenge_method' => 'S256',
|
||||
'ui_locales' => 'fr',
|
||||
];
|
||||
|
||||
header('Location: ' . $authEndpoint . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986), true, 302);
|
||||
exit;
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
||||
|
||||
require_once BASE_PATH . '/src/db.php';
|
||||
require_once BASE_PATH . '/src/PostManager.php';
|
||||
require_once BASE_PATH . '/src/FileManager.php';
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
|
||||
$postManager = new PostManager($db);
|
||||
$fileManager = new FileManager($db, __DIR__ . '/assets/uploads');
|
||||
|
||||
|
||||
// Gérer les accès
|
||||
// les fonctions create, delete, edit doit être autorisée aux personnes dont les roles leur permette
|
||||
|
||||
|
||||
|
||||
// Afficher la bonne page
|
||||
switch ($action) {
|
||||
case 'create':
|
||||
$title = $_POST['title'] ?? '';
|
||||
$content = $_POST['content'] ?? '';
|
||||
$published_at = $_POST['published_at'] ?? date('Y-m-d H:i:s');
|
||||
$published_at = str_replace('T', ' ', $published_at); // conversion HTML -> SQL
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (trim($title) === '') {
|
||||
$errors[] = 'Le titre est obligatoire.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$postId = $postManager->create($title, $content, $published_at);
|
||||
|
||||
if (!empty($_FILES['files']['name'][0])) {
|
||||
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
||||
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
||||
$file = [
|
||||
'name' => $_FILES['files']['name'][$i],
|
||||
'type' => $_FILES['files']['type'][$i],
|
||||
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
||||
'error' => $_FILES['files']['error'][$i],
|
||||
'size' => $_FILES['files']['size'][$i],
|
||||
];
|
||||
$fileManager->upload($postId, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: route.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$formAction = 'route.php?action=create';
|
||||
$action = 'create';
|
||||
include BASE_PATH . '/templates/post_form.php';
|
||||
break;
|
||||
|
||||
case 'view':
|
||||
if (!$id) {
|
||||
echo "ID manquant.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$post = $postManager->get($id);
|
||||
if (!$post) {
|
||||
echo "Post introuvable.";
|
||||
exit;
|
||||
}
|
||||
|
||||
include __DIR__ . '/../templates/post_view.php';
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if ($id) {
|
||||
$postManager->delete($id);
|
||||
}
|
||||
header("Location: route.php");
|
||||
exit;
|
||||
|
||||
case 'edit':
|
||||
if (!$id) {
|
||||
echo "ID manquant.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$post = $postManager->get($id);
|
||||
if (!$post) {
|
||||
echo "Post introuvable.";
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = $_POST['title'] ?? $post['title'];
|
||||
$content = $_POST['content'] ?? $post['content'];
|
||||
$published_at = $_POST['published_at'] ?? date('Y-m-d\TH:i', strtotime($post['created_at']));
|
||||
$published = isset($_POST['published']) ? true : $post['is_published'];
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (trim($title) === '') {
|
||||
$errors[] = 'Le titre est obligatoire.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$published_at_sql = str_replace('T', ' ', $_POST['published_at']);
|
||||
$postManager->update($id, $title, $content, $published_at_sql, $published);
|
||||
|
||||
if (!empty($_FILES['files']['name'][0])) {
|
||||
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
||||
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
||||
$file = [
|
||||
'name' => $_FILES['files']['name'][$i],
|
||||
'type' => $_FILES['files']['type'][$i],
|
||||
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
||||
'error' => $_FILES['files']['error'][$i],
|
||||
'size' => $_FILES['files']['size'][$i],
|
||||
];
|
||||
$fileManager->upload($id, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: route.php?action=view&id=$id");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$formAction = "route.php?action=edit&id=$id";
|
||||
$action = 'edit';
|
||||
include BASE_PATH . '/templates/post_form.php';
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
default:
|
||||
$posts = $postManager->getAll();
|
||||
include BASE_PATH . '/templates/post_list.php';
|
||||
break;
|
||||
}
|
||||
Reference in New Issue
Block a user