Merge pull request 'fix : vérification écriture settings + script setup.sh' (#63) from feat/wizard-multi-step into main
Reviewed-on: #63
This commit was merged in pull request #63.
This commit is contained in:
@@ -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
|
||||
|
||||
+104
-6
@@ -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;
|
||||
|
||||
@@ -2293,6 +2302,7 @@ switch ($action) {
|
||||
$tab = $_GET['tab'] ?? (isAdmin() ? 'dashboard' : 'articles');
|
||||
$adminData = [];
|
||||
$siteSettingsSaved = isset($_GET['saved']);
|
||||
$siteSettingsError = ($_GET['error'] ?? '') === 'write';
|
||||
|
||||
if ($tab === 'dashboard') {
|
||||
if (!isAdmin()) {
|
||||
@@ -2527,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();
|
||||
}
|
||||
@@ -2538,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;
|
||||
|
||||
@@ -2549,7 +2574,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 +2583,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 +2799,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 +2807,17 @@ 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_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':
|
||||
@@ -3119,6 +3154,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) ?? ''),
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/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 <groupe>."
|
||||
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"
|
||||
|
||||
# ─── 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 et PHP-FPM pour que le changement prenne effet :"
|
||||
echo " systemctl restart apache2 php8.3-fpm"
|
||||
else
|
||||
echo " Groupe adm absent, ignoré."
|
||||
fi
|
||||
|
||||
# ─── 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."
|
||||
+41
-18
@@ -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
|
||||
@@ -57,23 +56,32 @@ class SearchLogParser
|
||||
&& (time() - filemtime($this->cacheFile)) < $this->cacheTtl;
|
||||
}
|
||||
|
||||
/** @return list<array{path:string,gz:bool}> */
|
||||
/** @return list<array{path:string,type:string}> 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, 'gz' => false];
|
||||
}
|
||||
|
||||
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];
|
||||
// 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;
|
||||
}
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +90,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;
|
||||
|
||||
+13
-4
@@ -59,10 +59,19 @@ function siteLicenseUrl(): string
|
||||
return siteSettings()['site_license_url'] ?? 'https://creativecommons.org/licenses/by/4.0/';
|
||||
}
|
||||
|
||||
function saveSiteSettings(array $data): void
|
||||
function apacheAccessLog(): string
|
||||
{
|
||||
$fromSettings = siteSettings()['apache_access_log'] ?? '';
|
||||
if ($fromSettings !== '') {
|
||||
return $fromSettings;
|
||||
}
|
||||
return (string)($_ENV['APACHE_ACCESS_LOG'] ?? getenv('APACHE_ACCESS_LOG') ?: '*-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]);
|
||||
@@ -77,8 +86,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+192
-2
@@ -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>
|
||||
|
||||
@@ -94,11 +98,11 @@ function adminStatusBadge(array $a, int $now): string
|
||||
<!-- Version Folio ──────────────────────────────────────────────────────── -->
|
||||
<?php
|
||||
$_deployedVer = trim((string) @file_get_contents(BASE_PATH . '/public/version.txt'));
|
||||
$_deployedLabel = $_deployedVer !== '' ? date('d/m/Y H:i', strtotime($_deployedVer)) : '—';
|
||||
$_deployedLabel = $_deployedVer !== '' ? $_deployedVer : '—';
|
||||
$_notices = isset($_updateChecker) ? $_updateChecker->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 +468,8 @@ function adminStatusBadge(array $a, int $now): string
|
||||
|
||||
<?php if (!empty($siteSettingsSaved)): ?>
|
||||
<div class="alert alert-success py-2 mb-3">Paramètres enregistrés.</div>
|
||||
<?php elseif (!empty($siteSettingsError)): ?>
|
||||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card" style="max-width:540px">
|
||||
@@ -676,6 +682,8 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
|
||||
<?php if (isset($_GET['saved'])): ?>
|
||||
<div class="alert alert-success py-2 mb-3">Paramètres SMTP enregistrés.</div>
|
||||
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
|
||||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
@@ -1054,6 +1062,29 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
<!-- ─────────────────────────── RECHERCHES ─────────────────────────── -->
|
||||
<?php if ($tab === 'searches' && isAdmin()): ?>
|
||||
|
||||
<?php if (isset($_GET['saved'])): ?>
|
||||
<div class="alert alert-success py-2 mb-3">Configuration enregistrée.</div>
|
||||
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
|
||||
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card mb-4" style="max-width:540px">
|
||||
<div class="card-header bg-transparent py-2 small fw-semibold">Configuration des logs</div>
|
||||
<div class="card-body py-3">
|
||||
<form method="post" action="/?action=admin_save_searches_config">
|
||||
<div class="mb-3">
|
||||
<label for="apache-access-log" class="form-label small fw-semibold">Pattern des logs d'accès</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 : *-access.log">
|
||||
<div class="form-text">Pattern glob dans <code>/var/log/apache2/</code>. Les rotations (<code>.gz</code>, <code>.tar.gz</code>) sont automatiquement incluses.</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<h5 class="mb-0">Termes recherchés
|
||||
<span class="badge bg-secondary ms-1"><?= count($adminData['search_terms'] ?? []) ?></span>
|
||||
@@ -1110,6 +1141,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();
|
||||
|
||||
Reference in New Issue
Block a user