v1.6.25 — intégration IA éditeur, onglet admin IA, corrections CSP #98
@@ -5,6 +5,20 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
|
||||
|
||||
---
|
||||
|
||||
## [1.6.19] - 2026-05-16
|
||||
|
||||
### Ajouté
|
||||
- `admin/articles` : clic sur la ligne entière pour cocher/décocher la case de sélection bulk (#86)
|
||||
- Cache HTTP `Last-Modified` + réponse `304 Not Modified` pour les articles publiés (#18)
|
||||
- Fingerprinting des assets CSS/JS dans `layout.php` (`?v=<hash>`) pour invalidation automatique du cache navigateur (#18)
|
||||
- Cache fichier `_cache/articles_list.json` pour `getAll()` — invalidé à chaque écriture, élimine le scan complet par requête (#16)
|
||||
- Logging des 404 dans `DATA_PATH/_logs/not_found.json` (url, referer, user-agent, date) (#52)
|
||||
|
||||
### Corrigé
|
||||
- `case 'view'` : les accès refusés (brouillon, avant-première, catégorie privée) utilisent désormais `templates/404.php` au lieu d'un `echo` nu (#52)
|
||||
|
||||
---
|
||||
|
||||
## [1.6.18] - 2026-05-16
|
||||
|
||||
### Ajouté
|
||||
|
||||
@@ -19,6 +19,23 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Clic sur la ligne entière pour cocher/décocher la case de sélection
|
||||
document.querySelectorAll('table tbody tr').forEach(function (tr) {
|
||||
var cb = tr.querySelector('.bulk-check');
|
||||
if (!cb) { return; }
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.addEventListener('click', function (e) {
|
||||
if (e.target.closest('a, button, input, label')) { return; }
|
||||
cb.checked = !cb.checked;
|
||||
if (checkAll) {
|
||||
var total = document.querySelectorAll('.bulk-check').length;
|
||||
var checked = document.querySelectorAll('.bulk-check:checked').length;
|
||||
checkAll.checked = total > 0 && checked === total;
|
||||
checkAll.indeterminate = checked > 0 && checked < total;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Indicateurs de traitement formulaire SMTP (config + tester connexion)
|
||||
var smtpForm = document.getElementById('smtp-config-form');
|
||||
if (smtpForm) {
|
||||
|
||||
+46
-3
@@ -50,6 +50,25 @@ $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' :
|
||||
unset($_noindexActions);
|
||||
|
||||
// ─── Recherche de l'article le plus proche et redirection 301 ────────────────
|
||||
function log404(string $url): void
|
||||
{
|
||||
if (!defined('DATA_PATH')) {
|
||||
return;
|
||||
}
|
||||
$logDir = DATA_PATH . '/_logs';
|
||||
$logFile = $logDir . '/not_found.jsonl';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0755, true);
|
||||
}
|
||||
$entry = json_encode([
|
||||
'ts' => date('Y-m-d H:i:s'),
|
||||
'url' => $url,
|
||||
'ref' => $_SERVER['HTTP_REFERER'] ?? '',
|
||||
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
function slugToSearchQuery(string $rawPath): string
|
||||
{
|
||||
return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace(
|
||||
@@ -679,7 +698,7 @@ switch ($action) {
|
||||
if (!$article['published']) {
|
||||
if (!canDoOnArticle('view_drafts', $article)) {
|
||||
http_response_code(404);
|
||||
echo 'Article introuvable.';
|
||||
include BASE_PATH . '/templates/404.php';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -688,7 +707,7 @@ switch ($action) {
|
||||
if ($article['published'] && strtotime((string)($article['published_at'] ?? '')) > time()) {
|
||||
if (!hasCapability('view_previews')) {
|
||||
http_response_code(404);
|
||||
echo 'Article introuvable.';
|
||||
include BASE_PATH . '/templates/404.php';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -700,10 +719,33 @@ switch ($action) {
|
||||
$isPrivateCat = $articleCat !== '' && in_array($articleCat, $privateCats, true);
|
||||
if ($isPrivateCat && !isLoggedIn()) {
|
||||
http_response_code(404);
|
||||
echo 'Article introuvable.';
|
||||
include BASE_PATH . '/templates/404.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cache HTTP : Last-Modified + 304 pour les articles publiés
|
||||
if ($article['published']) {
|
||||
$_uuid = $article['uuid'] ?? '';
|
||||
$_mdFile = DATA_PATH . '/' . $_uuid . '/index.md';
|
||||
$_mfFile = DATA_PATH . '/' . $_uuid . '/meta.json';
|
||||
$_lm = max(
|
||||
is_file($_mdFile) ? (int)filemtime($_mdFile) : 0,
|
||||
is_file($_mfFile) ? (int)filemtime($_mfFile) : 0
|
||||
);
|
||||
if ($_lm > 0) {
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $_lm) . ' GMT');
|
||||
header('Cache-Control: public, max-age=60');
|
||||
$ifModSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||
: false;
|
||||
if ($ifModSince !== false && $_lm <= $ifModSince) {
|
||||
http_response_code(304);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
unset($_uuid, $_mdFile, $_mfFile, $_lm, $ifModSince);
|
||||
}
|
||||
|
||||
$files = $articles->getFiles($article['uuid']);
|
||||
|
||||
// Résout les chemins de fichiers relatifs dans le contenu
|
||||
@@ -3473,6 +3515,7 @@ switch ($action) {
|
||||
(string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''),
|
||||
'/'
|
||||
);
|
||||
log404('/' . $notFoundPath);
|
||||
$q = slugToSearchQuery($notFoundPath);
|
||||
header('Location: /search' . ($q !== '' ? '?q=' . urlencode($q) : ''), true, 302);
|
||||
exit;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.6.18
|
||||
1.6.19
|
||||
|
||||
+22
-1
@@ -30,6 +30,14 @@ class ArticleManager
|
||||
|
||||
private function loadAll(): array
|
||||
{
|
||||
$cachePath = $this->allListCachePath();
|
||||
if (file_exists($cachePath)) {
|
||||
$cached = json_decode((string)file_get_contents($cachePath), true);
|
||||
if (is_array($cached) && $cached !== []) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$articles = [];
|
||||
if (!is_dir($this->dataDir)) {
|
||||
return $articles;
|
||||
@@ -66,6 +74,12 @@ class ArticleManager
|
||||
}
|
||||
}
|
||||
|
||||
$cacheDir = dirname($cachePath);
|
||||
if (!is_dir($cacheDir)) {
|
||||
@mkdir($cacheDir, 0755, true);
|
||||
}
|
||||
@file_put_contents($cachePath, json_encode($articles, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $articles;
|
||||
}
|
||||
|
||||
@@ -849,6 +863,7 @@ class ArticleManager
|
||||
$this->allCache = null;
|
||||
@unlink($this->articleCachePath($uuid));
|
||||
@unlink($this->slugIndexPath());
|
||||
@unlink($this->allListCachePath());
|
||||
$this->removeDir($dir);
|
||||
}
|
||||
if (is_dir($dir)) {
|
||||
@@ -879,6 +894,11 @@ class ArticleManager
|
||||
return $this->dataDir . '/_cache/slug_index.json';
|
||||
}
|
||||
|
||||
private function allListCachePath(): string
|
||||
{
|
||||
return $this->dataDir . '/_cache/articles_list.json';
|
||||
}
|
||||
|
||||
private function buildSlugIndex(): void
|
||||
{
|
||||
$cacheDir = $this->dataDir . '/_cache';
|
||||
@@ -1343,9 +1363,10 @@ class ArticleManager
|
||||
$this->searchIndexCache = null;
|
||||
$uuid = $meta['uuid'] ?? basename($dir);
|
||||
|
||||
// Invalider le cache article et le slug index
|
||||
// Invalider les caches article, liste et slug index
|
||||
@unlink($this->articleCachePath($uuid));
|
||||
@unlink($this->slugIndexPath());
|
||||
@unlink($this->allListCachePath());
|
||||
|
||||
file_put_contents(
|
||||
$dir . '/meta.json',
|
||||
|
||||
+13
-4
@@ -46,11 +46,20 @@
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<?php
|
||||
$_pub = BASE_PATH . '/public/assets/';
|
||||
if (!function_exists('_av')) {
|
||||
function _av(string $base, string $rel): string {
|
||||
$f = $base . $rel;
|
||||
return '/assets/' . $rel . (is_file($f) ? '?v=' . substr(md5_file($f), 0, 8) : '');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= _av($_pub, 'css/style.css') ?>">
|
||||
</head>
|
||||
|
||||
<body<?php if (!empty($bodyClass ?? '')): ?> class="<?= htmlspecialchars($bodyClass) ?>"<?php endif; ?>>
|
||||
<script src="/assets/js/density-fouc.js"></script>
|
||||
<script src="<?= _av($_pub, 'js/density-fouc.js') ?>"></script>
|
||||
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark mb-0" role="navigation" aria-label="Navigation principale">
|
||||
@@ -150,9 +159,9 @@ $_layoutCurrentCat = trim($_GET['cat'] ?? '');
|
||||
|
||||
<!-- JS -->
|
||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
<script src="<?= _av($_pub, 'js/app.js') ?>"></script>
|
||||
<?php if (isset($reactionStats)): ?>
|
||||
<script src="/assets/js/reactions.js"></script>
|
||||
<script src="<?= _av($_pub, 'js/reactions.js') ?>"></script>
|
||||
<?php endif; ?>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user