diff --git a/public/.htaccess b/public/.htaccess index f46705f..26faccf 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -11,6 +11,35 @@ RewriteRule ^ - [L] # URL propre pour les articles : /post/ RewriteRule ^post/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=view&slug=$1 [L,QSA] +# Édition / création +RewriteRule ^edit/([0-9a-f-]{36})/?$ /index.php?action=edit&uuid=$1 [L,QSA] +RewriteRule ^new/?$ /index.php?action=create [L,QSA] +RewriteRule ^delete/([0-9a-f-]{36})/?$ /index.php?action=delete&uuid=$1 [L,QSA] + +# Sources et diff +RewriteRule ^sources/([0-9a-f-]{36})/?$ /index.php?action=sources&uuid=$1 [L,QSA] +RewriteRule ^diff/([0-9a-f-]{36})/(\d+)/?$ /index.php?action=diff&uuid=$1&rev=$2 [L,QSA] + +# Fichiers / import +RewriteRule ^files/([0-9a-f-]{36})/add/?$ /index.php?action=add_files&uuid=$1 [L,QSA] +RewriteRule ^import/([0-9a-f-]{36})/?$ /index.php?action=import_image&uuid=$1 [L,QSA] + +# Admin (regen-thumbs avant la règle générique admin/) +RewriteRule ^admin/regen-thumbs/?$ /index.php?action=regen_thumbs [L,QSA] +RewriteRule ^admin/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA] +RewriteRule ^admin/?$ /index.php?action=admin [L,QSA] + +# Pages de gestion +RewriteRule ^categories/?$ /index.php?action=categories [L,QSA] +RewriteRule ^profile/?$ /index.php?action=profile [L,QSA] +RewriteRule ^search/?$ /index.php?action=search [L,QSA] + +# Pages statiques +RewriteRule ^about/?$ /index.php?action=about [L,QSA] +RewriteRule ^legal/?$ /index.php?action=legal [L,QSA] +RewriteRule ^licenses/?$ /index.php?action=licenses [L,QSA] +RewriteRule ^contact/?$ /index.php?action=contact [L,QSA] + # Flux RSS — /feed, /rss et /rss.xml pointent tous vers feed.php RewriteRule ^feed/?$ /feed.php [L,QSA] RewriteRule ^rss/?$ /feed.php [L,QSA] diff --git a/public/index.php b/public/index.php index 3821b18..9a025ea 100644 --- a/public/index.php +++ b/public/index.php @@ -463,7 +463,7 @@ switch ($action) { } } - $formAction = '/?action=create'; + $formAction = '/new'; $action = 'create'; include BASE_PATH . '/templates/post_form.php'; break; @@ -637,7 +637,7 @@ switch ($action) { } } - $formAction = '/?action=edit&uuid=' . rawurlencode($uuid); + $formAction = '/edit/' . rawurlencode($uuid); $action = 'edit'; $existingFiles = $articles->getFiles($uuid); $insertUrl = ''; @@ -653,7 +653,7 @@ switch ($action) { if ($uuid !== '' && $fileName !== '' && $fileName[0] !== '.') { $articles->deleteFile($uuid, $fileName); } - header('Location: /?action=edit&uuid=' . rawurlencode($uuid)); + header('Location: /edit/' . rawurlencode($uuid)); exit; case 'delete': @@ -664,6 +664,30 @@ switch ($action) { header('Location: /'); exit; + case 'delete_revision': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($uuid !== '' && isset($_POST['rev_n'])) { + $articles->deleteRevision($uuid, (int)$_POST['rev_n']); + } + header('Location: /edit/' . rawurlencode($uuid) . '#historyPanel'); + exit; + + case 'delete_all_revisions': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + if ($uuid !== '') { + $articles->deleteAllRevisions($uuid); + } + header('Location: /edit/' . rawurlencode($uuid)); + exit; + case 'categories': requireAuth(); $cats = $articles->getCategories(); @@ -680,7 +704,7 @@ switch ($action) { $articles->renameCategory($old, $new); } } - header('Location: /?action=categories'); + header('Location: /categories'); exit; case 'delete_category': @@ -691,7 +715,7 @@ switch ($action) { $articles->deleteCategory($cat); } } - header('Location: /?action=categories'); + header('Location: /categories'); exit; case 'toggle_private_category': @@ -702,7 +726,7 @@ switch ($action) { $articles->togglePrivateCategory($cat); } } - header('Location: /?action=categories'); + header('Location: /categories'); exit; case 'about': @@ -740,7 +764,7 @@ switch ($action) { } } if ($revIndex === null || $revN < 1) { - header('Location: /?action=edit&uuid=' . rawurlencode($uuid)); + header('Location: /edit/' . rawurlencode($uuid)); exit; } $oldContent = $articles->getRevisionContent($uuid, $revN); @@ -789,7 +813,7 @@ switch ($action) { ]); } } - header('Location: /?action=edit&uuid=' . rawurlencode($uuid)); + header('Location: /edit/' . rawurlencode($uuid)); exit; } include BASE_PATH . '/templates/add_files.php'; @@ -816,7 +840,7 @@ switch ($action) { case 'import_image_step2': requireAuth(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - header('Location: /?action=import_image&uuid=' . rawurlencode($uuid)); + header('Location: /import/' . rawurlencode($uuid)); exit; } $step2Article = $articles->getByUuid($uuid); @@ -827,12 +851,12 @@ switch ($action) { } $step2Url = trim($_POST['image_url'] ?? ''); if (!filter_var($step2Url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $step2Url)) { - header('Location: /?action=import_image&uuid=' . rawurlencode($uuid) . '&error=1'); + header('Location: /import/' . rawurlencode($uuid) . '?error=1'); exit; } $step2Meta = fetchUrlMeta($step2Url); if (!($step2Meta['ok'] ?? false)) { - header('Location: /?action=import_image&uuid=' . rawurlencode($uuid) . '&error=1'); + header('Location: /import/' . rawurlencode($uuid) . '?error=1'); exit; } // Capture d'écran pour prévisualisation (pages HTML uniquement) @@ -862,7 +886,7 @@ switch ($action) { $ackUrl = filter_var($_GET['image_url'] ?? '', FILTER_VALIDATE_URL) ? $_GET['image_url'] : ''; if ($ackUrl === '') { - header('Location: /?action=import_image&uuid=' . rawurlencode($uuid)); + header('Location: /import/' . rawurlencode($uuid)); exit; } $ackTitle = $_GET['img_title'] ?? ''; @@ -903,7 +927,7 @@ switch ($action) { $urlArticle = $articles->getByUuid($urlUuid); if (!$urlArticle || $imageUrl === '' || !filter_var($imageUrl, FILTER_VALIDATE_URL)) { - header('Location: /?action=import_image&uuid=' . rawurlencode($urlUuid)); + header('Location: /import/' . rawurlencode($urlUuid)); exit; } @@ -911,13 +935,13 @@ switch ($action) { if ($mode === 'screenshot') { if ($screenshotFile === '' || $screenshotFile !== '_preview.png') { - header('Location: /?action=import_image&uuid=' . rawurlencode($urlUuid) . '&error=1'); + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); exit; } $filesDir = BASE_PATH . '/data/' . $urlUuid . '/files'; $previewPath = $filesDir . '/' . $screenshotFile; if (!file_exists($previewPath)) { - header('Location: /?action=import_image&uuid=' . rawurlencode($urlUuid) . '&error=1'); + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1'); exit; } $hash = substr(hash_file('sha256', $previewPath), 0, 16); @@ -928,7 +952,7 @@ switch ($action) { if ($isCover) { $articles->setCover($urlUuid, $destName); } - header('Location: /?action=edit&uuid=' . rawurlencode($urlUuid)); + header('Location: /edit/' . rawurlencode($urlUuid)); exit; } @@ -993,7 +1017,7 @@ switch ($action) { @unlink($filesDir . '/' . $screenshotFile); } $articles->addExternalLink($urlUuid, $imageUrl, $imgTitle, $imgAuthor, $importedMeta); - header('Location: /?action=edit&uuid=' . rawurlencode($urlUuid)); + header('Location: /edit/' . rawurlencode($urlUuid)); exit; } @@ -1013,9 +1037,9 @@ switch ($action) { $imported = $articles->addFileFromUrl($urlUuid, $imageUrl, $isCover, $imgAuthor, $imgSource, $imgTitle, $importedMeta); if ($imported) { - header('Location: /?action=edit&uuid=' . rawurlencode($urlUuid)); + header('Location: /edit/' . rawurlencode($urlUuid)); } else { - header('Location: /?action=import_image&uuid=' . rawurlencode($urlUuid) . '&error=1&mode=download'); + header('Location: /import/' . rawurlencode($urlUuid) . '?error=1&mode=download'); } exit; @@ -1044,8 +1068,7 @@ switch ($action) { ob_start(); ?>

Génération des aperçus de liens

-
- +
@@ -1195,7 +1218,7 @@ switch ($action) { echo $done . ' capturé' . ($done > 1 ? 's' : '') . ', '; echo $fail . ' échec' . ($fail > 1 ? 's' : '') . ', '; echo $skip . ' ignoré' . ($skip > 1 ? 's' : '') . '.

'; - echo '← Retour'; + echo '← Retour'; echo ''; exit; @@ -1207,7 +1230,7 @@ switch ($action) { $articles->removeExternalLink($uuid, $linkUrl); } } - header('Location: /?action=edit&uuid=' . rawurlencode($uuid)); + header('Location: /edit/' . rawurlencode($uuid)); exit; case 'rate': @@ -1368,7 +1391,7 @@ switch ($action) { $st->execute([':email' => $targetEmail, ':role' => $roleName, ':by' => currentUserEmail()]); } } - header('Location: /?action=admin&tab=users'); + header('Location: /admin/users'); exit; case 'admin_revoke_role': @@ -1390,7 +1413,7 @@ switch ($action) { $st->execute([':email' => $targetEmail, ':role' => $roleName]); } } - header('Location: /?action=admin&tab=users'); + header('Location: /admin/users'); exit; case 'admin_create_role': @@ -1411,7 +1434,7 @@ switch ($action) { } } } - header('Location: /?action=admin&tab=roles'); + header('Location: /admin/roles'); exit; case 'admin_update_role': @@ -1429,7 +1452,7 @@ switch ($action) { $st->execute([':l' => $roleLabel, ':id' => $roleId]); } } - header('Location: /?action=admin&tab=roles'); + header('Location: /admin/roles'); exit; case 'admin_delete_role': @@ -1446,7 +1469,7 @@ switch ($action) { $st->execute([':id' => $roleId]); } } - header('Location: /?action=admin&tab=roles'); + header('Location: /admin/roles'); exit; case 'admin_update_role_caps': @@ -1469,7 +1492,7 @@ switch ($action) { unset($_SESSION['user_capabilities']); } } - header('Location: /?action=admin&tab=roles'); + header('Location: /admin/roles'); exit; case 'profile': diff --git a/public/oidc/callback.php b/public/oidc/callback.php index 8a59dab..603082f 100644 --- a/public/oidc/callback.php +++ b/public/oidc/callback.php @@ -2,10 +2,6 @@ declare(strict_types=1); -if (session_status() !== PHP_SESSION_ACTIVE) { - session_start(); -} - require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once dirname(__DIR__, 2) . '/bootstrap.php'; require_once dirname(__DIR__, 2) . '/config/config.php'; @@ -29,7 +25,7 @@ $debug = (env('APP_DEBUG', '0') === '1'); $OIDC_ISSUER = rtrim((string)(env('OIDC_ISSUER') ?? ''), '/'); $OIDC_CLIENT_ID = (string)(env('OIDC_CLIENT_ID') ?? ''); $OIDC_CLIENT_SECRET = (string)(env('OIDC_CLIENT_SECRET') ?? ''); -$OIDC_REDIRECT_URI = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback.php')); +$OIDC_REDIRECT_URI = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback')); if (!$OIDC_ISSUER || !$OIDC_CLIENT_ID || !$OIDC_REDIRECT_URI) { http_response_code(500); diff --git a/public/oidc/me.php b/public/oidc/me.php index b9620cc..91cf8f5 100644 --- a/public/oidc/me.php +++ b/public/oidc/me.php @@ -4,10 +4,6 @@ // version : 20251005 declare(strict_types=1); -if (session_status() !== PHP_SESSION_ACTIVE) { - session_start(); -} - require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once dirname(__DIR__, 2) . '/bootstrap.php'; require_once dirname(__DIR__, 2) . '/config/config.php'; diff --git a/public/oidc/start.php b/public/oidc/start.php index 46418b6..5461ec8 100644 --- a/public/oidc/start.php +++ b/public/oidc/start.php @@ -2,10 +2,6 @@ declare(strict_types=1); -if (session_status() !== PHP_SESSION_ACTIVE) { - session_start(); -} - require_once dirname(__DIR__, 2) . '/vendor/autoload.php'; require_once dirname(__DIR__, 2) . '/bootstrap.php'; require_once dirname(__DIR__, 2) . '/config/config.php'; diff --git a/src/ArticleManager.php b/src/ArticleManager.php index 4e5fbf2..859e9b8 100644 --- a/src/ArticleManager.php +++ b/src/ArticleManager.php @@ -40,7 +40,7 @@ class ArticleManager $articles[] = $article; } - usort($articles, static fn ($a, $b) => strcmp($b['created_at'] ?? '', $a['created_at'] ?? '')); + usort($articles, static fn ($a, $b) => strcmp($b['published_at'] ?? '', $a['published_at'] ?? '')); return $articles; } @@ -131,28 +131,33 @@ class ArticleManager $slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title); $slug = $this->uniqueSlug($slug, $uuid); - // Snapshot de l'état courant avant écrasement - $revisions = $article['revisions'] ?? []; - $revDir = $this->dataDir . '/' . $uuid . '/revisions'; - if (!is_dir($revDir)) { - mkdir($revDir, 0755, true); - } - $n = count($revisions) + 1; - $revFile = sprintf('%s/%04d.md', $revDir, $n); - file_put_contents($revFile, $article['content']); + // Snapshot de l'état courant avant écrasement — uniquement si le contenu ou le titre a changé + $revisions = $article['revisions'] ?? []; + $contentChanged = ltrim($content) !== ($article['content'] ?? ''); + $titleChanged = $title !== ($article['title'] ?? ''); - $revisions[] = [ - 'n' => $n, - 'date' => date('Y-m-d H:i:s'), - 'comment' => $revisionComment, - 'title' => $article['title'], - ]; + if ($contentChanged || $titleChanged) { + $revDir = $this->dataDir . '/' . $uuid . '/revisions'; + if (!is_dir($revDir)) { + mkdir($revDir, 0755, true); + } + $n = count($revisions) + 1; + $revFile = sprintf('%s/%04d.md', $revDir, $n); + file_put_contents($revFile, $article['content']); - // Limite à MAX_REVISIONS - if (count($revisions) > self::MAX_REVISIONS) { - $oldest = array_shift($revisions); - @unlink(sprintf('%s/%04d.md', $revDir, (int)($oldest['n'] ?? 0))); - } + $revisions[] = [ + 'n' => $n, + 'date' => date('Y-m-d H:i:s'), + 'comment' => $revisionComment, + 'title' => $article['title'], + ]; + + // Limite à MAX_REVISIONS + if (count($revisions) > self::MAX_REVISIONS) { + $oldest = array_shift($revisions); + @unlink(sprintf('%s/%04d.md', $revDir, (int)($oldest['n'] ?? 0))); + } + } // fin if ($contentChanged || $titleChanged) $meta = [ 'uuid' => $uuid, @@ -211,10 +216,6 @@ class ArticleManager return; } $filename = basename($filename); - $path = $this->dataDir . '/' . $uuid . '/files/' . $filename; - if (!file_exists($path)) { - return; - } $raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json'); if ($raw === false) { @@ -774,6 +775,50 @@ class ArticleManager return $meta; } + public function deleteRevision(string $uuid, int $revN): void + { + if (!$this->isValidUuid($uuid) || $revN < 1) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + $raw = @file_get_contents($dir . '/meta.json'); + $meta = $raw !== false ? json_decode($raw, true) : null; + if (!is_array($meta)) { + return; + } + $revisions = $meta['revisions'] ?? []; + $newRevisions = []; + foreach ($revisions as $rev) { + if ((int)($rev['n'] ?? 0) === $revN) { + @unlink(sprintf('%s/revisions/%04d.md', $dir, $revN)); + } else { + $newRevisions[] = $rev; + } + } + $meta['revisions'] = array_values($newRevisions); + $this->writeMeta($dir, $meta); + } + + public function deleteAllRevisions(string $uuid): void + { + if (!$this->isValidUuid($uuid)) { + return; + } + $dir = $this->dataDir . '/' . $uuid; + $revDir = $dir . '/revisions'; + if (is_dir($revDir)) { + foreach (glob($revDir . '/*.md') ?: [] as $f) { + @unlink($f); + } + } + $raw = @file_get_contents($dir . '/meta.json'); + $meta = $raw !== false ? json_decode($raw, true) : null; + if (is_array($meta)) { + $meta['revisions'] = []; + $this->writeMeta($dir, $meta); + } + } + private function writeMeta(string $dir, array $meta): void { file_put_contents( diff --git a/templates/about.php b/templates/about.php index 7d9dc34..058eba6 100644 --- a/templates/about.php +++ b/templates/about.php @@ -85,7 +85,7 @@ ob_start();

- Vous pouvez me joindre via le formulaire de contact. + Vous pouvez me joindre via le formulaire de contact. Je lis tous les messages, même si je ne réponds pas toujours vite.

diff --git a/templates/add_files.php b/templates/add_files.php index b8ba4cd..057b3a6 100644 --- a/templates/add_files.php +++ b/templates/add_files.php @@ -4,7 +4,7 @@ $existingFiles = $articles->getFiles($addFilesArticle['uuid']); ?>
- ← Retour + ← Retour

Ajouter des fichiers

@@ -19,7 +19,7 @@ $existingFiles = $articles->getFiles($addFilesArticle['uuid']);
@@ -34,7 +34,7 @@ $existingFiles = $articles->getFiles($addFilesArticle['uuid']);
- Annuler
diff --git a/templates/admin.php b/templates/admin.php index 56edf41..c65d0fe 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -17,7 +17,7 @@ function adminStatusBadge(array $a, int $now): string

Administration

- + Nouvel article + + Nouvel article
@@ -25,24 +25,24 @@ function adminStatusBadge(array $a, int $now): string @@ -132,7 +132,7 @@ function adminStatusBadge(array $a, int $now): string - Modifier diff --git a/templates/copyright_ack.php b/templates/copyright_ack.php index 7fa3f1e..fe47e19 100644 --- a/templates/copyright_ack.php +++ b/templates/copyright_ack.php @@ -4,7 +4,7 @@
- ← Retour

Confirmation — droits d'auteur

@@ -99,7 +99,7 @@
- Annuler
diff --git a/templates/diff.php b/templates/diff.php index 2d1d940..39d6837 100644 --- a/templates/diff.php +++ b/templates/diff.php @@ -4,7 +4,7 @@ $revMeta = $revisions[$revIndex] ?? []; ?>
- ← Retour + ← Retour
— révision # diff --git a/templates/import_image.php b/templates/import_image.php index 37d359d..7e48911 100644 --- a/templates/import_image.php +++ b/templates/import_image.php @@ -1,7 +1,7 @@
- ← Retour + ← Retour

Importer un fichier depuis une URL

@@ -28,7 +28,7 @@
- Annuler
diff --git a/templates/import_image_step2.php b/templates/import_image_step2.php index ab37316..af1a732 100644 --- a/templates/import_image_step2.php +++ b/templates/import_image_step2.php @@ -40,7 +40,7 @@ $preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url; ?>
- ← Retour + ← Retour

Importer un fichier

@@ -192,7 +192,7 @@ if ($visibleRows): ?>
- Annuler
diff --git a/templates/layout.php b/templates/layout.php index 3140162..365b87f 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -100,11 +100,11 @@
diff --git a/templates/legal.php b/templates/legal.php index ebe0b9c..3b0dbe0 100644 --- a/templates/legal.php +++ b/templates/legal.php @@ -35,7 +35,7 @@ ob_start();

Responsable de publication : Cédric Abonnel

Qualité : Particulier — site personnel non commercial

-

Contact : formulaire de contact

+

Contact : formulaire de contact

@@ -74,7 +74,7 @@ ob_start();

Les composants tiers (Bootstrap, PHPMailer, police Inter…) sont soumis à leurs licences respectives, - détaillées sur la page des licences. + détaillées sur la page des licences.

@@ -99,7 +99,7 @@ ob_start();

Conformément au RGPD (règlement UE 2016/679), vous disposez d'un droit d'accès, de rectification et de suppression des données vous concernant. Pour exercer ces droits : - formulaire de contact. + formulaire de contact.

diff --git a/templates/post_form.php b/templates/post_form.php index 726c1a7..b3776a1 100644 --- a/templates/post_form.php +++ b/templates/post_form.php @@ -102,96 +102,6 @@ $dateValue = isset($published_at) - - - -
-

Fichiers existants

-
- $f): ?> - -
-
- - - - - - - - '🎬', - str_starts_with($f['mime'], 'audio/') => '🎵', - $f['mime'] === 'application/pdf' => '📑', - default => '📄', - } ?> - - - - -
-
- - Ko - - cover - -
- -
- - - -
- -
- - -
- -
- - -
- - - ✓ Cover - -
- - -
-
-
-
- -
-
- - @@ -285,6 +195,91 @@ $dateValue = isset($published_at)
+ + + +
+

Fichiers existants

+
+ $f): ?> + +
+
+ + + + + + + + '🎬', + str_starts_with($f['mime'], 'audio/') => '🎵', + $f['mime'] === 'application/pdf' => '📑', + default => '📄', + } ?> + + + + +
+
+ + Ko + + cover + +
+
+ + + +
+
+ +
+ + +
+ + + ✓ Cover + +
+ + +
+
+
+
+
+ +
+
+
+ + $f['is_image']); ?> @@ -341,17 +336,17 @@ $dateValue = isset($published_at)
- + + Ajouter des fichiers - + + Importer depuis une URL - +if ($hasSources): + ?> + Sources & métadonnées @@ -373,10 +368,18 @@ $dateValue = isset($published_at)
- +
+ +
+ +
+
@@ -384,8 +387,9 @@ $dateValue = isset($published_at) + - + @@ -395,11 +399,18 @@ $dateValue = isset($published_at) - diff --git a/templates/post_list.php b/templates/post_list.php index c37891f..094e7a9 100644 --- a/templates/post_list.php +++ b/templates/post_list.php @@ -54,10 +54,10 @@ ob_start(); Disponible le - + - modifier + modifier→ lire diff --git a/templates/post_view.php b/templates/post_view.php index b305791..582b406 100644 --- a/templates/post_view.php +++ b/templates/post_view.php @@ -32,7 +32,7 @@ if ($files) { $externalLinks = $article['external_links'] ?? []; $hasLeftSidebar = !empty($categorySidebar ?? []); ?> -
+
–' ?> - + Diff +
+ + +