Initial commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php // includes/ConfigRepo.php
|
||||
declare(strict_types=1);
|
||||
|
||||
function config_repo_get(): array {
|
||||
$pdo = db();
|
||||
$row = $pdo->query("SELECT * FROM app_config WHERE id=1")->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) { return [
|
||||
'allow_password'=>true,'allow_oidc'=>false,'registrations_open'=>true,
|
||||
'oidc_issuer'=>null,'oidc_name'=>null,'oidc_client_id'=>null,'oidc_client_secret'=>null,'oidc_redirect_uri'=>null
|
||||
]; }
|
||||
return $row;
|
||||
}
|
||||
|
||||
function config_repo_save(array $in): void {
|
||||
$pdo = db();
|
||||
$sql = "INSERT INTO app_config
|
||||
(id, allow_password, allow_oidc, registrations_open, oidc_issuer, oidc_name, oidc_client_id, oidc_client_secret, oidc_redirect_uri, updated_at)
|
||||
VALUES (1,:pw,:oidc,:open,:iss,:name,:cid,:sec,:redir, now())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
allow_password=:pw, allow_oidc=:oidc, registrations_open=:open,
|
||||
oidc_issuer=:iss, oidc_name=:name, oidc_client_id=:cid, oidc_client_secret=:sec, oidc_redirect_uri=:redir,
|
||||
updated_at=now()";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':pw' => (bool)$in['allow_password'],
|
||||
':oidc' => (bool)$in['allow_oidc'],
|
||||
':open' => (bool)$in['registrations_open'],
|
||||
':iss' => trim((string)($in['oidc_issuer'] ?? '')) ?: null,
|
||||
':name' => trim((string)($in['oidc_name'] ?? '')) ?: null,
|
||||
':cid' => trim((string)($in['oidc_client_id'] ?? '')) ?: null,
|
||||
':sec' => trim((string)($in['oidc_client_secret'] ?? '')) ?: null,
|
||||
':redir'=> trim((string)($in['oidc_redirect_uri'] ?? '')) ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le fichier .env en conservant les autres lignes.
|
||||
* $pairs = ['KEY'=>'value', ...] ; value null => supprime la clé.
|
||||
*/
|
||||
function env_set_pairs(string $envPath, array $pairs): void {
|
||||
if (!is_file($envPath)) { file_put_contents($envPath, ""); }
|
||||
$lines = file($envPath, FILE_IGNORE_NEW_LINES);
|
||||
$map = [];
|
||||
foreach ($lines as $i => $line) {
|
||||
if (preg_match('/^\s*#/', $line) || trim($line)==='') { $map[$i] = $line; continue; }
|
||||
if (!str_contains($line, '=')) { $map[$i] = $line; continue; }
|
||||
[$k,$v] = explode('=', $line, 2);
|
||||
$k = trim($k);
|
||||
if ($k==='') { $map[$i] = $line; continue; }
|
||||
if (array_key_exists($k, $pairs)) {
|
||||
if ($pairs[$k] === null) { $map[$i] = null; } // supprimé
|
||||
else { $map[$i] = $k.'='.env_quote((string)$pairs[$k]); }
|
||||
unset($pairs[$k]);
|
||||
} else {
|
||||
$map[$i] = $line;
|
||||
}
|
||||
}
|
||||
// append keys restantes
|
||||
foreach ($pairs as $k=>$v) {
|
||||
if ($v === null) continue;
|
||||
$map[] = $k.'='.env_quote((string)$v);
|
||||
}
|
||||
// re-écriture
|
||||
$out = [];
|
||||
foreach ($map as $line) { if ($line === null) continue; $out[] = $line; }
|
||||
file_put_contents($envPath, implode(PHP_EOL, $out).PHP_EOL);
|
||||
}
|
||||
|
||||
function env_quote(string $v): string {
|
||||
if ($v === '' || preg_match('/\s|[#"\'=]/', $v)) {
|
||||
// met entre guillemets et échappe
|
||||
$v = str_replace(['\\','"'], ['\\\\','\\"'], $v);
|
||||
return "\"$v\"";
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
function ensure_admin(): void {
|
||||
// adapte à ton système
|
||||
if (empty($_SESSION['user']['is_admin'])) {
|
||||
http_response_code(403);
|
||||
exit('Forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<footer class="py-3 my-4">
|
||||
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
|
||||
<li class="nav-item"><a href="https://alpinux.org/mentions-legales" class="nav-link px-2 text-body-secondary">Mentions légales</a></li>
|
||||
<li class="nav-item"><a href="/index/a-propos" class="nav-link px-2 text-body-secondary">A propos</a></li>
|
||||
</ul>
|
||||
<p class="text-center text-body-secondary">Association 1901 - <a href="https://alpinux.org/">Alpinux, le LUG de Savoie</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
|
||||
<div class="container">
|
||||
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
|
||||
<a href="/" class="d-flex align-items-center text-body-emphasis text-decoration-none">
|
||||
<img width="32" src="/img/logo-mail.svg" class="bi me-2" >
|
||||
<span class="fs-4">Mug ALPINUX</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
<?php
|
||||
// Créer une instance de MessageManager avec le fichier de base de données SQLite
|
||||
$messageManager = new ace\MessageManager('database.db');
|
||||
|
||||
if ($messageManager->sessionAlready()) {
|
||||
?>
|
||||
|
||||
<div class="dropdown text-end">
|
||||
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<?php echo $messageManager->getUsername($_SESSION['user_id']); ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu text-small">
|
||||
<li><a class="dropdown-item" href="/user/parametres">Paramètres <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sliders" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z"/>
|
||||
</svg></a></li>
|
||||
<li><a class="dropdown-item" href="/user/profil">Profil <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-vcard" viewBox="0 0 16 16">
|
||||
<path d="M5 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm4-2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5ZM9 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 9 8Zm1 2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5Z"/>
|
||||
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2ZM1 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8.96c.026-.163.04-.33.04-.5C9 10.567 7.21 9 5 9c-2.086 0-3.8 1.398-3.984 3.181A1.006 1.006 0 0 1 1 12V4Z"/>
|
||||
</svg></a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/user/disconnect">Déconnexion <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
// projet : mug.a5l.fr
|
||||
// fichier : includes/mailer.php
|
||||
// version : 20251011
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.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;
|
||||
}
|
||||
}
|
||||
if (!function_exists('db')) {
|
||||
function db(): \PDO { return \App\Infrastructure\Database::get(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Anti-abus simple : 1 envoi / 5 min et 5 en 12 h par destinataire (status in ('sent','queued')).
|
||||
*/
|
||||
function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): array {
|
||||
// bypass complet si désactivé
|
||||
$enabled = (int) (env('SMTP_RATE_LIMIT_ENABLE', '1'));
|
||||
if ($enabled === 0) return [true, ''];
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Cooldown (actif seulement si >0)
|
||||
if ($coolMin > 0) {
|
||||
$q1 = "SELECT 1 FROM journal_smtp
|
||||
WHERE to_email = :e AND created_at >= NOW() - INTERVAL :cool
|
||||
AND status IN ('sent','queued')
|
||||
LIMIT 1";
|
||||
$stmt = $pdo->prepare($q1);
|
||||
$stmt->execute([':e'=>$email, ':cool'=>sprintf('%d minutes', $coolMin)]);
|
||||
if ($stmt->fetchColumn()) return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."];
|
||||
}
|
||||
|
||||
// Plafond 12h (actif seulement si >0)
|
||||
if ($maxPer12h > 0) {
|
||||
$q2 = "SELECT COUNT(*) FROM journal_smtp
|
||||
WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours'
|
||||
AND status IN ('sent','queued')";
|
||||
$stmt = $pdo->prepare($q2);
|
||||
$stmt->execute([':e'=>$email]);
|
||||
if ((int)$stmt->fetchColumn() >= $maxPer12h) return [false, 'Quota atteint. Réessayez plus tard.'];
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Envoi immédiat SMTP avec PHPMailer + journalisation.
|
||||
* @param string $to destinataire
|
||||
* @param string $subject objet
|
||||
* @param string $html corps HTML
|
||||
* @param string|null $text corps texte brut (optionnel, auto-généré si null)
|
||||
* @param array $opts ['reply_to'=>['email','name']]
|
||||
*/
|
||||
function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool {
|
||||
[$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5'));
|
||||
if (!$ok) throw new RuntimeException($msg);
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO journal_smtp
|
||||
(created_at, script_path, to_email, subject, content_html, content_text, status, ip, user_agent)
|
||||
VALUES (NOW(), :script, :to, :subj, :html, :text, 'queued', :ip, :ua)
|
||||
RETURNING id");
|
||||
$stmt->execute([
|
||||
':script' => ($_SERVER['SCRIPT_NAME'] ?? ''),
|
||||
':to' => $to,
|
||||
':subj' => $subject,
|
||||
':html' => $html,
|
||||
':text' => $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES)),
|
||||
':ip' => ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''),
|
||||
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 512),
|
||||
]);
|
||||
$rowId = (int)$stmt->fetchColumn();
|
||||
$pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = (string)env('SMTP_HOST', 'localhost');
|
||||
$mail->Port = (int)env('SMTP_PORT', '587');
|
||||
$mail->SMTPAuth = (env('SMTP_USER') || env('SMTP_PASS')) ? true : false;
|
||||
$mail->Username = (string)env('SMTP_USER', '');
|
||||
$mail->Password = (string)env('SMTP_PASS', '');
|
||||
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
|
||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
|
||||
$mail->SMTPKeepAlive = true; // réutilise la connexion
|
||||
$mail->Timeout = 30; // évite les blocages longs
|
||||
$mail->SMTPOptions = ['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true,'allow_self_signed'=>false]];
|
||||
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->isHTML(true);
|
||||
|
||||
// Expéditeur
|
||||
$from = (string)env('SMTP_FROM', 'no-reply@mug.a5l.fr');
|
||||
$fromName = (string)env('SMTP_FROM_NAME', 'MUG');
|
||||
$mail->setFrom($from, $fromName);
|
||||
|
||||
// Reply-To
|
||||
if (!empty($opts['reply_to']) && is_array($opts['reply_to']) && filter_var($opts['reply_to'][0] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addReplyTo($opts['reply_to'][0], $opts['reply_to'][1] ?? '');
|
||||
} elseif ($rt = env('SMTP_REPLY_TO')) {
|
||||
$mail->addReplyTo($rt, (string)env('SMTP_REPLY_TO_NAME', 'Support'));
|
||||
}
|
||||
|
||||
// DKIM optionnel
|
||||
if ($d = env('DKIM_DOMAIN')) {
|
||||
$mail->DKIM_domain = $d;
|
||||
$mail->DKIM_selector = (string)env('DKIM_SELECTOR', 'default');
|
||||
$mail->DKIM_private = (string)env('DKIM_PRIVATE_KEY_PATH', '');
|
||||
$mail->DKIM_passphrase = (string)env('DKIM_PASSPHRASE', '');
|
||||
$mail->DKIM_identity = $from;
|
||||
}
|
||||
|
||||
$mail->addAddress($to);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $html;
|
||||
$mail->AltBody = $text ?? trim(html_entity_decode(strip_tags($html), ENT_QUOTES));
|
||||
|
||||
$mail->send();
|
||||
|
||||
$pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id'=>$rowId]);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id")
|
||||
->execute([':id'=>$rowId, ':err'=>substr($e->getMessage(),0,1000)]);
|
||||
throw new RuntimeException('Envoi email impossible: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user