feat : log Apache configurable via Administration → Site (apache_access_log)

Ajoute apacheAccessLog() dans SiteSettings — priorité au réglage admin,
fallback sur APACHE_ACCESS_LOG dans .env. Champ ajouté dans le formulaire
Site de l'administration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 00:16:49 +02:00
parent d488bcd00c
commit d18f9abd16
4 changed files with 274 additions and 3 deletions
+4
View File
@@ -39,3 +39,7 @@ SMTP_FROM_NAME=
# Formulaire de contact
CONTACT_EMAIL=
CONTACT_FROM_EMAIL=
# Logs Apache (onglet Recherches dans /admin)
# Nom du fichier de log d'accès du vhost dans /var/log/apache2/
APACHE_ACCESS_LOG=lan.acegrp.varlog-access.log
+89 -2
View File
@@ -23,8 +23,10 @@ require_once BASE_PATH . '/src/helpers.php';
require_once BASE_PATH . '/src/auth.php';
require_once BASE_PATH . '/src/SiteSettings.php';
require_once BASE_PATH . '/src/ArticleManager.php';
require_once BASE_PATH . '/src/BookManager.php';
$articles = new ArticleManager(BASE_PATH . '/data');
$books = new BookManager(BASE_PATH . '/data/books');
// ─── Mode maintenance ──────────────────────────────────────────────────────
if (file_exists(BASE_PATH . '/data/.maintenance')) {
@@ -41,7 +43,7 @@ $action = $_GET['action'] ?? 'list';
$uuid = $_GET['uuid'] ?? '';
$slug = $_GET['slug'] ?? '';
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate', 'comment_delete', 'comment_resend', 'create_tag_type', 'delete_tag_type', 'edit_tags'];
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate', 'comment_delete', 'comment_resend', 'create_tag_type', 'delete_tag_type', 'edit_tags', 'book_save', 'book_delete'];
$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null;
unset($_noindexActions);
@@ -825,6 +827,13 @@ switch ($action) {
$comments = $commentMgr->forArticle($article['uuid']);
}
// Contexte livre (navigation précédent/suivant si l'article fait partie d'un livre)
$bookContext = $books->findForArticle($article['slug'] ?? '');
if ($bookContext !== null) {
$bookContext['prev_article'] = $bookContext['prev'] !== null ? $articles->getBySlug($bookContext['prev']) : null;
$bookContext['next_article'] = $bookContext['next'] !== null ? $articles->getBySlug($bookContext['next']) : null;
}
include BASE_PATH . '/templates/post_view.php';
break;
@@ -2528,7 +2537,7 @@ switch ($action) {
exit;
}
require_once BASE_PATH . '/src/SearchLogParser.php';
$parser = new SearchLogParser();
$parser = new SearchLogParser('/var/log/apache2', apacheAccessLog());
$adminData['search_terms'] = $parser->topTerms(100);
$adminData['search_log_readable'] = $parser->isReadable();
}
@@ -2539,6 +2548,21 @@ switch ($action) {
$adminData['tagTypes'] = $articles->getTagTypes();
}
if ($tab === 'books') {
if (!isAdmin()) {
http_response_code(403);
exit;
}
$adminData['books'] = $books->getAll();
$adminData['edit_book'] = null;
$adminData['all_articles'] = $articles->getAll();
usort($adminData['all_articles'], static fn ($a, $b) => strcmp($a['title'] ?? '', $b['title'] ?? ''));
$editBookSlug = trim($_GET['edit'] ?? '');
if ($editBookSlug !== '') {
$adminData['edit_book'] = $books->getBySlug($editBookSlug);
}
}
include BASE_PATH . '/templates/admin.php';
break;
@@ -3120,6 +3144,69 @@ switch ($action) {
include BASE_PATH . '/templates/search.php';
break;
case 'book':
$bookSlug = trim($_GET['book_slug'] ?? '');
$book = $books->getBySlug($bookSlug);
if (!$book) {
http_response_code(404);
ob_start();
?>
<div class="container py-5 text-center">
<h1 class="h2 mb-3">Livre introuvable</h1>
<p class="text-muted mb-4">Ce livre n'existe pas ou a été supprimé.</p>
<a href="/" class="btn btn-primary">← Retour à l'accueil</a>
</div>
<?php
$content = ob_get_clean();
$title = '404 — ' . siteTitle();
$metaRobots = 'noindex, nofollow';
include BASE_PATH . '/templates/layout.php';
break;
}
$bookArticles = [];
foreach ($book['articles'] ?? [] as $aSlug) {
$a = $articles->getBySlug($aSlug);
if (!$a) {
continue;
}
if (!$a['published'] && !canDoOnArticle('view_drafts', $a)) {
continue;
}
$bookArticles[] = $a;
}
$allCats = $articles->getCategories();
include BASE_PATH . '/templates/book.php';
break;
case 'book_save':
requireAuth();
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit;
}
$bSlug = trim($_POST['slug'] ?? '');
$bTitle = trim($_POST['title'] ?? '');
$bDesc = trim($_POST['description'] ?? '');
$bArts = array_values(array_filter(array_map('trim', preg_split('/[\r\n]+/', $_POST['articles'] ?? ''))));
if ($bSlug !== '' && $bTitle !== '') {
$books->save(['slug' => $bSlug, 'title' => $bTitle, 'description' => $bDesc, 'articles' => $bArts]);
}
header('Location: /admin/books?saved=1&edit=' . rawurlencode($bSlug));
exit;
case 'book_delete':
requireAuth();
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit;
}
$bSlug = trim($_POST['slug'] ?? '');
if ($bSlug !== '') {
$books->delete($bSlug);
}
header('Location: /admin/books?deleted=1');
exit;
case 'not_found':
$notFoundPath = trim(
(string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''),
+10 -1
View File
@@ -59,10 +59,19 @@ function siteLicenseUrl(): string
return siteSettings()['site_license_url'] ?? 'https://creativecommons.org/licenses/by/4.0/';
}
function apacheAccessLog(): string
{
$fromSettings = siteSettings()['apache_access_log'] ?? '';
if ($fromSettings !== '') {
return $fromSettings;
}
return (string)($_ENV['APACHE_ACCESS_LOG'] ?? getenv('APACHE_ACCESS_LOG') ?: 'lan.acegrp.varlog-access.log');
}
function saveSiteSettings(array $data): bool
{
$current = siteSettings();
$stringKeys = ['site_title', 'site_claim', 'site_lang', 'site_license_label', 'site_license_url'];
$stringKeys = ['site_title', 'site_claim', 'site_lang', 'site_license_label', 'site_license_url', 'apache_access_log'];
foreach ($stringKeys as $key) {
if (array_key_exists($key, $data)) {
$val = trim((string)$data[$key]);
+171
View File
@@ -65,6 +65,10 @@ function adminStatusBadge(array $a, int $now): string
<a class="nav-link <?= $tab === 'searches' ? 'active' : '' ?>"
href="/admin/searches">Recherches</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'books' ? 'active' : '' ?>"
href="/admin/books">Livres</a>
</li>
<?php endif; ?>
</ul>
@@ -503,6 +507,14 @@ function adminStatusBadge(array $a, int $now): string
value="<?= postsPerPage() ?>"
min="1" max="100">
</div>
<div class="mb-3">
<label for="apache-access-log" class="form-label small fw-semibold">Log d'accès Apache</label>
<input type="text" id="apache-access-log" name="apache_access_log"
class="form-control form-control-sm font-monospace"
value="<?= htmlspecialchars(apacheAccessLog()) ?>"
maxlength="200" placeholder="ex : lan.acegrp.monsite-access.log">
<div class="form-text">Nom du fichier dans <code>/var/log/apache2/</code>, utilisé par l'onglet Recherches.</div>
</div>
<div class="mb-3">
<label for="site-license-label" class="form-label small fw-semibold">Licence (libellé)</label>
<input type="text" id="site-license-label" name="site_license_label"
@@ -1114,6 +1126,165 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
<?php endif; ?>
<!-- ─────────────────────────── LIVRES ─────────────────────────── -->
<?php if ($tab === 'books' && isAdmin()): ?>
<?php if (($_GET['deleted'] ?? '') === '1'): ?>
<div class="alert alert-success py-2 small mb-3">Livre supprimé.</div>
<?php endif; ?>
<div class="row g-4">
<!-- Liste des livres -->
<div class="col-md-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
Livres
<span class="badge bg-secondary ms-1"><?= count($adminData['books']) ?></span>
</h5>
<a href="/admin/books?new=1" class="btn btn-sm btn-primary">+ Nouveau</a>
</div>
<?php if (empty($adminData['books'])): ?>
<p class="text-muted small">Aucun livre pour l'instant.</p>
<?php else: ?>
<div class="list-group">
<?php foreach ($adminData['books'] as $bk):
$isEdited = ($adminData['edit_book']['slug'] ?? '') === $bk['slug'];
?>
<a href="/admin/books?edit=<?= rawurlencode($bk['slug']) ?>"
class="list-group-item list-group-item-action<?= $isEdited ? ' active' : '' ?>">
<div class="fw-medium"><?= htmlspecialchars($bk['title']) ?></div>
<div class="small <?= $isEdited ? 'text-white-50' : 'text-muted' ?>">
<?= count($bk['articles'] ?? []) ?> page<?= count($bk['articles'] ?? []) > 1 ? 's' : '' ?>
· <a href="/book/<?= rawurlencode($bk['slug']) ?>" target="_blank"
class="<?= $isEdited ? 'text-white-50' : 'text-muted' ?>">Voir </a>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Formulaire édition / création -->
<div class="col-md-8">
<?php if (($adminData['edit_book'] ?? null) !== null): ?>
<?php $eb = $adminData['edit_book']; ?>
<h5>Modifier le livre</h5>
<?php if (($_GET['saved'] ?? '') === '1'): ?>
<div class="alert alert-success py-2 small">Livre sauvegardé.</div>
<?php endif; ?>
<form method="POST" action="/?action=book_save">
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
<div class="mb-3">
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($eb['slug']) ?>" readonly>
<div class="form-text">Le slug ne peut pas être modifié après création.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Titre</label>
<input type="text" name="title" class="form-control" required
value="<?= htmlspecialchars($eb['title'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Description</label>
<textarea name="description" class="form-control" rows="2"><?= htmlspecialchars($eb['description'] ?? '') ?></textarea>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Pages (slugs dans l'ordre, un par ligne)</label>
<textarea name="articles" class="form-control font-monospace"
id="book-articles-ta"
rows="<?= max(6, count($eb['articles'] ?? []) + 2) ?>"><?= htmlspecialchars(implode("\n", $eb['articles'] ?? [])) ?></textarea>
<div class="form-text">Un slug par ligne. L'ordre définit la navigation précédent/suivant.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Ajouter une page existante</label>
<select class="form-select" onchange="bookAddArticle(this)">
<option value=""> Choisir un article </option>
<?php
$alreadyIn = $eb['articles'] ?? [];
foreach ($adminData['all_articles'] as $aa):
if (in_array($aa['slug'] ?? '', $alreadyIn, true)) {
continue;
}
?>
<option value="<?= htmlspecialchars($aa['slug'] ?? '') ?>">
<?= htmlspecialchars($aa['title']) ?>
<?= !$aa['published'] ? ' (brouillon)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Sauvegarder</button>
<a href="/book/<?= rawurlencode($eb['slug']) ?>" target="_blank"
class="btn btn-outline-secondary">Voir le livre </a>
</div>
</form>
<hr class="my-4">
<form method="POST" action="/?action=book_delete"
data-confirm="Supprimer le livre « <?= htmlspecialchars($eb['title']) ?> » ? Les pages resteront intactes.">
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">🗑 Supprimer ce livre</button>
</form>
<script>
function bookAddArticle(sel) {
var slug = sel.value;
if (!slug) return;
var ta = document.getElementById('book-articles-ta');
var lines = ta.value.split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
if (lines.indexOf(slug) === -1) {
lines.push(slug);
ta.value = lines.join('\n');
}
sel.value = '';
}
</script>
<?php elseif (isset($_GET['new'])): ?>
<h5>Nouveau livre</h5>
<form method="POST" action="/?action=book_save">
<div class="mb-3">
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
<input type="text" name="slug" class="form-control" required
placeholder="mon-livre" pattern="[a-z0-9][a-z0-9-]*">
<div class="form-text">Minuscules, chiffres, tirets. Exemple : <code>esp8266</code></div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Titre</label>
<input type="text" name="title" class="form-control" required placeholder="Titre du livre">
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Description (optionnelle)</label>
<textarea name="description" class="form-control" rows="2" placeholder="Courte description…"></textarea>
</div>
<input type="hidden" name="articles" value="">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Créer le livre</button>
<a href="/admin/books" class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
<?php else: ?>
<p class="text-muted mt-2">
<?php if (!empty($adminData['books'])): ?>
Sélectionnez un livre à gauche pour le modifier, ou créez-en un nouveau.
<?php else: ?>
Cliquez sur <strong>+ Nouveau</strong> pour créer votre premier livre.
<?php endif; ?>
</p>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
$title = 'Administration — ' . siteTitle();