From 9091a00a32a3762fb2898d77f6bb7de98b3a0cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:02:54 +0200 Subject: [PATCH 1/7] =?UTF-8?q?fix=20:=20saveSiteSettings=20et=20saveSmtpS?= =?UTF-8?q?ettings=20retournent=20bool,=20erreur=20affich=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file_put_contents() échouait silencieusement (permissions), provoquant un saved=1 trompeur. Les deux fonctions retournent maintenant bool ; les callers redirigent vers ?error=write et le template affiche un message d'erreur explicite. Co-Authored-By: Claude Sonnet 4.6 --- public/index.php | 9 +++++---- src/SiteSettings.php | 6 +++--- src/SmtpSettings.php | 6 +++--- templates/admin.php | 8 ++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/public/index.php b/public/index.php index 1a0bdcf..b9f361f 100644 --- a/public/index.php +++ b/public/index.php @@ -2293,6 +2293,7 @@ switch ($action) { $tab = $_GET['tab'] ?? (isAdmin() ? 'dashboard' : 'articles'); $adminData = []; $siteSettingsSaved = isset($_GET['saved']); + $siteSettingsError = ($_GET['error'] ?? '') === 'write'; if ($tab === 'dashboard') { if (!isAdmin()) { @@ -2549,7 +2550,7 @@ switch ($action) { } require_once BASE_PATH . '/src/SmtpSettings.php'; - saveSmtpSettings([ + $ok = saveSmtpSettings([ 'host' => $_POST['smtp_host'] ?? '', 'port' => $_POST['smtp_port'] ?? '', 'secure' => $_POST['smtp_secure'] ?? '', @@ -2558,7 +2559,7 @@ switch ($action) { 'from' => $_POST['smtp_from'] ?? '', 'from_name' => $_POST['smtp_from_name'] ?? '', ]); - header('Location: /admin/smtp?saved=1'); + header('Location: /admin/smtp?' . ($ok ? 'saved=1' : 'error=write')); exit; case 'admin_smtp_test': @@ -2774,7 +2775,7 @@ switch ($action) { http_response_code(403); exit; } - saveSiteSettings([ + $ok = saveSiteSettings([ 'site_title' => $_POST['site_title'] ?? '', 'site_claim' => $_POST['site_claim'] ?? '', 'site_lang' => $_POST['site_lang'] ?? '', @@ -2782,7 +2783,7 @@ switch ($action) { 'site_license_label' => $_POST['site_license_label'] ?? '', 'site_license_url' => $_POST['site_license_url'] ?? '', ]); - header('Location: /admin/site?saved=1'); + header('Location: /admin/site?' . ($ok ? 'saved=1' : 'error=write')); exit; case 'admin_create_role': diff --git a/src/SiteSettings.php b/src/SiteSettings.php index 6c4bbc1..c0570cd 100644 --- a/src/SiteSettings.php +++ b/src/SiteSettings.php @@ -59,7 +59,7 @@ function siteLicenseUrl(): string return siteSettings()['site_license_url'] ?? 'https://creativecommons.org/licenses/by/4.0/'; } -function saveSiteSettings(array $data): void +function saveSiteSettings(array $data): bool { $current = siteSettings(); $stringKeys = ['site_title', 'site_claim', 'site_lang', 'site_license_label', 'site_license_url']; @@ -77,8 +77,8 @@ function saveSiteSettings(array $data): void $current['posts_per_page'] = $val; } } - file_put_contents( + return file_put_contents( siteSettingsPath(), json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) - ); + ) !== false; } diff --git a/src/SmtpSettings.php b/src/SmtpSettings.php index a054959..a4b0765 100644 --- a/src/SmtpSettings.php +++ b/src/SmtpSettings.php @@ -35,7 +35,7 @@ function smtpCfg(string $key, string $envKey, string $default = ''): string return ($v !== false && $v !== '') ? (string)$v : $default; } -function saveSmtpSettings(array $data): void +function saveSmtpSettings(array $data): bool { $current = smtpSettings(); foreach (['host', 'port', 'secure', 'user', 'from', 'from_name'] as $key) { @@ -46,8 +46,8 @@ function saveSmtpSettings(array $data): void if (!empty($data['pass']) && trim((string)$data['pass']) !== '') { $current['pass'] = trim((string)$data['pass']); } - file_put_contents( + return file_put_contents( smtpSettingsPath(), json_encode($current, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) - ); + ) !== false; } diff --git a/templates/admin.php b/templates/admin.php index 67716ae..a2b850f 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -94,11 +94,11 @@ function adminStatusBadge(array $a, int $now): string adminNotices() : []; $_remoteLabel = '—'; foreach ($_notices as $_n) { - if ($_n['type'] === 'info' && preg_match('/publiée le ([^)]+)/', $_n['message'], $_m)) { + if ($_n['type'] === 'info' && preg_match('/v([\d]+\.[\d]+\.[\d]+)/', $_n['message'], $_m)) { $_remoteLabel = $_m[1]; } } @@ -464,6 +464,8 @@ function adminStatusBadge(array $a, int $now): string
Paramètres enregistrés.
+ +
Impossible d'enregistrer : le fichier n'est pas accessible en écriture.
@@ -676,6 +678,8 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
Paramètres SMTP enregistrés.
+ +
Impossible d'enregistrer : le fichier n'est pas accessible en écriture.
From fd2397ff90a28334908de6da86e6d893d0728d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:10:30 +0200 Subject: [PATCH 2/7] =?UTF-8?q?feat=20:=20script=20setup.sh=20pour=20le=20?= =?UTF-8?q?d=C3=A9ploiement=20initial=20de=20Folio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crée les dossiers requis, applique setgid sur data/ et _cache/ pour que les fichiers héritent du groupe web (www-data) quelle que soit leur origine (PHP ou rsync), installe les dépendances et lance les migrations SQL. Co-Authored-By: Claude Sonnet 4.6 --- scripts/setup.sh | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100755 scripts/setup.sh diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..8bff73a --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# setup.sh — déploiement initial de Folio sur un serveur +# +# Usage : sudo ./scripts/setup.sh [--web-group www-data] [--data-dir /chemin/data] +# +# Ce script est idempotent : il peut être relancé sans risque. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$SCRIPT_DIR/.." + +# ─── Paramètres ────────────────────────────────────────────────────────────── +WEB_GROUP="www-data" +DATA_DIR="$ROOT/data" + +while [[ $# -gt 0 ]]; do + case "$1" in + --web-group) WEB_GROUP="$2"; shift 2 ;; + --data-dir) DATA_DIR="$2"; shift 2 ;; + *) echo "Option inconnue : $1"; exit 1 ;; + esac +done + +# ─── Vérifications préalables ───────────────────────────────────────────────── +if [[ ! -f "$ROOT/.env" ]]; then + echo "✗ Fichier .env manquant. Copier .env.example en .env et remplir les valeurs." + exit 1 +fi + +if ! getent group "$WEB_GROUP" > /dev/null 2>&1; then + echo "✗ Groupe '$WEB_GROUP' introuvable. Utiliser --web-group ." + exit 1 +fi + +# ─── Dépendances PHP ────────────────────────────────────────────────────────── +echo "→ Installation des dépendances PHP..." +composer install --no-dev --optimize-autoloader --working-dir="$ROOT" + +# ─── Dossiers requis ───────────────────────────────────────────────────────── +echo "→ Création des dossiers..." +mkdir -p "$DATA_DIR" +mkdir -p "$ROOT/public/_cache" +mkdir -p "$ROOT/.sessions" + +# ─── Permissions ───────────────────────────────────────────────────────────── +echo "→ Configuration des permissions (groupe : $WEB_GROUP)..." + +# data/ : setgid pour héritage de groupe + lecture/écriture propriétaire et groupe +chown -R :"$WEB_GROUP" "$DATA_DIR" +chmod g+s "$DATA_DIR" +find "$DATA_DIR" -type d -exec chmod g+s {} + +find "$DATA_DIR" -exec chmod ug+rw {} + + +# public/_cache/ : créé et écrit par PHP +chown -R :"$WEB_GROUP" "$ROOT/public/_cache" +chmod g+s "$ROOT/public/_cache" +find "$ROOT/public/_cache" -exec chmod ug+rw {} + + +# .sessions/ : écrit par PHP +chown -R :"$WEB_GROUP" "$ROOT/.sessions" +chmod 770 "$ROOT/.sessions" + +# ─── Migrations SQL ─────────────────────────────────────────────────────────── +echo "→ Migrations SQL..." +php "$ROOT/database/migrate.php" + +echo "" +echo "✓ Folio est prêt." +echo " Vérifier que APP_URL et ADMIN_EMAIL sont corrects dans .env." From 157c30f20c9ed3aaec010df5b09cc45211562466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:11:51 +0200 Subject: [PATCH 3/7] =?UTF-8?q?feat=20:=20setup.sh=20=E2=80=94=20ajout=20d?= =?UTF-8?q?e=20www-data=20au=20groupe=20adm=20pour=20les=20logs=20Apache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- scripts/setup.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/setup.sh b/scripts/setup.sh index 8bff73a..23b59f9 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -60,6 +60,15 @@ find "$ROOT/public/_cache" -exec chmod ug+rw {} + chown -R :"$WEB_GROUP" "$ROOT/.sessions" chmod 770 "$ROOT/.sessions" +# ─── Groupe adm pour lecture des logs Apache ───────────────────────────────── +echo "→ Ajout de $WEB_GROUP au groupe adm (logs Apache)..." +if getent group adm > /dev/null 2>&1; then + usermod -aG adm "$WEB_GROUP" + echo " Redémarrer Apache pour que le changement prenne effet : systemctl restart apache2" +else + echo " Groupe adm absent, ignoré." +fi + # ─── Migrations SQL ─────────────────────────────────────────────────────────── echo "→ Migrations SQL..." php "$ROOT/database/migrate.php" From d488bcd00cef361e8924d05c79cbe8cc76fa4233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:12:31 +0200 Subject: [PATCH 4/7] =?UTF-8?q?fix=20:=20setup.sh=20=E2=80=94=20rappel=20r?= =?UTF-8?q?ed=C3=A9marrage=20PHP-FPM=20en=20plus=20d'Apache=20pour=20group?= =?UTF-8?q?e=20adm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- scripts/setup.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 23b59f9..6b2a80a 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -64,7 +64,8 @@ chmod 770 "$ROOT/.sessions" echo "→ Ajout de $WEB_GROUP au groupe adm (logs Apache)..." if getent group adm > /dev/null 2>&1; then usermod -aG adm "$WEB_GROUP" - echo " Redémarrer Apache pour que le changement prenne effet : systemctl restart apache2" + echo " Redémarrer Apache et PHP-FPM pour que le changement prenne effet :" + echo " systemctl restart apache2 php8.3-fpm" else echo " Groupe adm absent, ignoré." fi From d18f9abd16158fd35bce329fb1c8b52f8ce7e6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:16:49 +0200 Subject: [PATCH 5/7] =?UTF-8?q?feat=20:=20log=20Apache=20configurable=20vi?= =?UTF-8?q?a=20Administration=20=E2=86=92=20Site=20(apache=5Faccess=5Flog)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 4 + public/index.php | 91 ++++++++++++++++++++++- src/SiteSettings.php | 11 ++- templates/admin.php | 171 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 75db2a2..55820d9 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/public/index.php b/public/index.php index b9f361f..386f6c8 100644 --- a/public/index.php +++ b/public/index.php @@ -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(); + ?> +
+

Livre introuvable

+

Ce livre n'existe pas ou a été supprimé.

+ ← Retour à l'accueil +
+ 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) ?? ''), diff --git a/src/SiteSettings.php b/src/SiteSettings.php index c0570cd..1d9d08f 100644 --- a/src/SiteSettings.php +++ b/src/SiteSettings.php @@ -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]); diff --git a/templates/admin.php b/templates/admin.php index a2b850f..88ce490 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -65,6 +65,10 @@ function adminStatusBadge(array $a, int $now): string Recherches + @@ -503,6 +507,14 @@ function adminStatusBadge(array $a, int $now): string value="" min="1" max="100">
+
+ + +
Nom du fichier dans /var/log/apache2/, utilisé par l'onglet Recherches.
+
$_rgb): + + + + +
Livre supprimé.
+ + +
+ + +
+
+
+ Livres + +
+ + Nouveau +
+ + +

Aucun livre pour l'instant.

+ + + +
+ + +
+ + +
Modifier le livre
+ + +
Livre sauvegardé.
+ + +
+ +
+ + +
Le slug ne peut pas être modifié après création.
+
+
+ + +
+
+ + +
+
+ + +
Un slug par ligne. L'ordre définit la navigation précédent/suivant.
+
+
+ + +
+
+ + Voir le livre ↗ +
+
+ +
+ +
+ + +
+ + + + +
Nouveau livre
+
+
+ + +
Minuscules, chiffres, tirets. Exemple : esp8266
+
+
+ + +
+
+ + +
+ +
+ + Annuler +
+
+ + +

+ + Sélectionnez un livre à gauche pour le modifier, ou créez-en un nouveau. + + Cliquez sur + Nouveau pour créer votre premier livre. + +

+ +
+ +
+ + + Date: Fri, 15 May 2026 00:26:16 +0200 Subject: [PATCH 6/7] feat : SearchLogParser supporte tar.gz + config log dans onglet Recherches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - logFiles() utilise glob() au lieu d'un range fixe 1-14 - Support .tar.gz via PharData - Champ apache_access_log déplacé du tab Site vers un bloc dédié dans le tab Recherches (action admin_save_searches_config) Co-Authored-By: Claude Sonnet 4.6 --- public/index.php | 10 ++++++++++ src/SearchLogParser.php | 38 ++++++++++++++++++++++++++++---------- templates/admin.php | 31 +++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/public/index.php b/public/index.php index 386f6c8..092b3b9 100644 --- a/public/index.php +++ b/public/index.php @@ -2810,6 +2810,16 @@ switch ($action) { header('Location: /admin/site?' . ($ok ? 'saved=1' : 'error=write')); exit; + case 'admin_save_searches_config': + requireAuth(); + if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(403); + exit; + } + $ok = saveSiteSettings(['apache_access_log' => $_POST['apache_access_log'] ?? '']); + header('Location: /admin/searches?' . ($ok ? 'saved=1' : 'error=write')); + exit; + case 'admin_create_role': requireAuth(); if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { diff --git a/src/SearchLogParser.php b/src/SearchLogParser.php index cb7c3b5..b9af3a9 100644 --- a/src/SearchLogParser.php +++ b/src/SearchLogParser.php @@ -57,23 +57,26 @@ class SearchLogParser && (time() - filemtime($this->cacheFile)) < $this->cacheTtl; } - /** @return list */ + /** @return list type: plain|gz|tgz */ private function logFiles(): array { $base = $this->logDir . '/' . $this->vhostBase; $files = []; if (file_exists($base) && is_readable($base)) { - $files[] = ['path' => $base, 'gz' => false]; + $files[] = ['path' => $base, 'type' => 'plain']; } - for ($i = 1; $i <= 14; $i++) { - $plain = $base . '.' . $i; - $gz = $plain . '.gz'; - if (file_exists($plain) && is_readable($plain)) { - $files[] = ['path' => $plain, 'gz' => false]; - } elseif (file_exists($gz) && is_readable($gz)) { - $files[] = ['path' => $gz, 'gz' => true]; + foreach (glob($base . '.*') ?: [] as $path) { + if (!is_readable($path)) { + continue; + } + if (str_ends_with($path, '.tar.gz')) { + $files[] = ['path' => $path, 'type' => 'tgz']; + } elseif (str_ends_with($path, '.gz')) { + $files[] = ['path' => $path, 'type' => 'gz']; + } else { + $files[] = ['path' => $path, 'type' => 'plain']; } } @@ -82,7 +85,22 @@ class SearchLogParser private function parseFile(array $file, array &$counts): void { - if ($file['gz']) { + if ($file['type'] === 'tgz') { + try { + $phar = new PharData($file['path']); + foreach ($phar as $entry) { + $content = @file_get_contents('phar://' . $file['path'] . '/' . $entry->getFilename()); + if ($content === false) { + continue; + } + foreach (explode("\n", $content) as $line) { + $this->parseLine($line, $counts); + } + } + } catch (\Exception $e) { + // archive illisible, on ignore + } + } elseif ($file['type'] === 'gz') { $h = @gzopen($file['path'], 'rb'); if (!$h) { return; diff --git a/templates/admin.php b/templates/admin.php index 88ce490..32579dd 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -507,14 +507,6 @@ function adminStatusBadge(array $a, int $now): string value="" min="1" max="100">
-
- - -
Nom du fichier dans /var/log/apache2/, utilisé par l'onglet Recherches.
-
$_rgb): + +
Configuration enregistrée.
+ +
Impossible d'enregistrer : le fichier n'est pas accessible en écriture.
+ + +
+
Configuration des logs
+
+
+
+ + +
Nom du fichier dans /var/log/apache2/. Supporte .gz et .tar.gz.
+
+ +
+
+
+
Termes recherchés From 3bb83b3ffdccb44d27653ad840a1443a7bf2ae49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Fri, 15 May 2026 00:35:19 +0200 Subject: [PATCH 7/7] =?UTF-8?q?feat=20:=20SearchLogParser=20accepte=20un?= =?UTF-8?q?=20pattern=20glob=20pour=20les=20logs=20d'acc=C3=A8s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Balaye tous les fichiers correspondant au pattern (ex: *-access.log) et leurs rotations .gz/.tar.gz. Valeur par défaut : *-access.log. Label renommé en "Pattern des logs d'accès". Co-Authored-By: Claude Sonnet 4.6 --- src/SearchLogParser.php | 39 ++++++++++++++++++++++----------------- src/SiteSettings.php | 2 +- templates/admin.php | 6 +++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/SearchLogParser.php b/src/SearchLogParser.php index b9af3a9..63b092e 100644 --- a/src/SearchLogParser.php +++ b/src/SearchLogParser.php @@ -11,7 +11,7 @@ class SearchLogParser public function __construct( string $logDir = '/var/log/apache2', - string $vhostBase = 'lan.acegrp.varlog-access.log', + string $vhostBase = '*-access.log', string $cacheFile = '', int $cacheTtl = 600 ) { @@ -47,8 +47,7 @@ class SearchLogParser public function isReadable(): bool { - $f = $this->logDir . '/' . $this->vhostBase; - return file_exists($f) && is_readable($f); + return count($this->logFiles()) > 0; } private function cacheValid(): bool @@ -60,23 +59,29 @@ class SearchLogParser /** @return list type: plain|gz|tgz */ private function logFiles(): array { - $base = $this->logDir . '/' . $this->vhostBase; - $files = []; + $pattern = $this->logDir . '/' . $this->vhostBase; + $files = []; - if (file_exists($base) && is_readable($base)) { - $files[] = ['path' => $base, 'type' => 'plain']; - } - - foreach (glob($base . '.*') ?: [] as $path) { - if (!is_readable($path)) { + // Fichiers correspondant au pattern de base (courants + rotations incluses si glob) + $bases = glob($pattern) ?: []; + // Ajouter aussi les rotations (.N, .N.gz, .N.tar.gz) pour chaque base trouvée + foreach ($bases as $base) { + // Exclure les rotations déjà capturées par le pattern glob + if (str_ends_with($base, '.gz') || preg_match('/\.\d+$/', $base)) { continue; } - if (str_ends_with($path, '.tar.gz')) { - $files[] = ['path' => $path, 'type' => 'tgz']; - } elseif (str_ends_with($path, '.gz')) { - $files[] = ['path' => $path, 'type' => 'gz']; - } else { - $files[] = ['path' => $path, 'type' => 'plain']; + $candidates = array_merge([$base], glob($base . '.*') ?: []); + foreach ($candidates as $path) { + if (!is_readable($path)) { + continue; + } + if (str_ends_with($path, '.tar.gz')) { + $files[] = ['path' => $path, 'type' => 'tgz']; + } elseif (str_ends_with($path, '.gz')) { + $files[] = ['path' => $path, 'type' => 'gz']; + } else { + $files[] = ['path' => $path, 'type' => 'plain']; + } } } diff --git a/src/SiteSettings.php b/src/SiteSettings.php index 1d9d08f..856d0ae 100644 --- a/src/SiteSettings.php +++ b/src/SiteSettings.php @@ -65,7 +65,7 @@ function apacheAccessLog(): string if ($fromSettings !== '') { return $fromSettings; } - return (string)($_ENV['APACHE_ACCESS_LOG'] ?? getenv('APACHE_ACCESS_LOG') ?: 'lan.acegrp.varlog-access.log'); + return (string)($_ENV['APACHE_ACCESS_LOG'] ?? getenv('APACHE_ACCESS_LOG') ?: '*-access.log'); } function saveSiteSettings(array $data): bool diff --git a/templates/admin.php b/templates/admin.php index 32579dd..dc4dd46 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -1073,12 +1073,12 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
- + -
Nom du fichier dans /var/log/apache2/. Supporte .gz et .tar.gz.
+ maxlength="200" placeholder="ex : *-access.log"> +
Pattern glob dans /var/log/apache2/. Les rotations (.gz, .tar.gz) sont automatiquement incluses.