Fix: /admin/content is nu boom-editor + image-grootte knop + custom syntax

- /admin/content is nu de boom-editor (plugin-file-tree structuur),
  consistent met plugin- en thema-editors. Oude tabelweergave verhuisd
  naar /admin/content-list, bereikbaar via 'Boom weergave'/'Lijst
  weergave' knoppen. /admin/content-files redirect naar /admin/content.
- Image-grootte knop in markdown editor-toolbar: selecteer ![alt](url),
  klik knop, breedte/hoogte dialog, {:width=... height=...} syntax
  toevoegen/vervangen. Bestaande waarden worden in prompts getoond.
- Custom-grootte syntax {:width=...} nu ook correct herschreven naar
  /-media/ endpoint (was alleen gewone ![alt](url) herschreven).
- Handleidingen bijgewerkt (content-beheer.md NL+EN: boom/lijst
  weergave uitleg + image-grootte + images in content sectie)
- Release notes v2.6.2 bijgewerkt
- Pentest 30/30, WCAG 25/25
This commit is contained in:
2026-08-19 12:15:56 +00:00
parent e926a3a40d
commit 97c4d52c78
17 changed files with 171 additions and 81 deletions
+56 -51
View File
@@ -270,12 +270,17 @@ switch ($route) {
break;
case 'content':
handleContentFiles($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
case 'content-list':
handleContent($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
case 'content-files':
handleContentFiles($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
// Backward compat: redirect to the unified content editor
header('Location: /admin/content');
exit;
case 'content-file-upload':
handleContentFileUpload($auth, $appConfig, $user);
@@ -1015,18 +1020,18 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
function handleContentFileUpload($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1040,13 +1045,13 @@ function handleContentFileUpload($auth, $config, $user): void
$realTarget = realpath($targetDir);
if ($realTarget === false) {
if (!@mkdir($targetDir, 0755, true)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$realTarget = realpath($targetDir);
}
if (!$realTarget || strpos($realTarget, $realContentDir) !== 0 || !is_dir($realTarget)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1090,7 +1095,7 @@ function handleContentFileUpload($auth, $config, $user): void
$type = $type === 'success' ? 'success' : 'danger';
}
$redir = '/admin/content-files';
$redir = '/admin/content';
if (!empty($msg)) {
$redir .= '?msg=' . urlencode(implode(' ', $msg)) . '&msgtype=' . urlencode($type);
}
@@ -1105,18 +1110,18 @@ function handleContentFileUpload($auth, $config, $user): void
function handleContentFileDelete($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1124,20 +1129,20 @@ function handleContentFileDelete($auth, $config, $user): void
$relFile = ltrim($relFile, '/');
foreach (explode('/', $relFile) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
}
$base = basename($relFile);
if ($base === '' || $base[0] === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$fullPath = $realContentDir . '/' . $relFile;
$realPath = realpath($fullPath);
if ($realPath === false || strpos($realPath, $realContentDir) !== 0 || !is_file($realPath)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1152,7 +1157,7 @@ function handleContentFileDelete($auth, $config, $user): void
$type = 'danger';
}
header('Location: /admin/content-files?msg=' . urlencode($msg) . '&msgtype=' . urlencode($type));
header('Location: /admin/content?msg=' . urlencode($msg) . '&msgtype=' . urlencode($type));
exit;
}
@@ -1165,7 +1170,7 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1173,20 +1178,20 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
$relFile = ltrim($relFile, '/');
foreach (explode('/', $relFile) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
}
$base = basename($relFile);
if ($base === '' || $base[0] === '.') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$fullPath = $realContentDir . '/' . $relFile;
$realPath = realpath($fullPath);
if ($realPath === false || strpos($realPath, $realContentDir) !== 0 || !is_file($realPath)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1241,7 +1246,7 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
} elseif (rename($realPath, $newPath)) {
adminLog($config, 'info', $user['username'] . ' verplaatste content/' . $relFile . ' naar content/' . ($dest ? $dest . '/' : '') . basename($relFile));
$newRel = $dest === '' ? basename($relFile) : $dest . '/' . basename($relFile);
header('Location: /admin/content-files?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand verplaatst.') . '&msgtype=success');
header('Location: /admin/content?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand verplaatst.') . '&msgtype=success');
exit;
} else {
$err = error_get_last();
@@ -1275,18 +1280,18 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
function handleContentDirCreateIn($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1298,19 +1303,19 @@ function handleContentDirCreateIn($auth, $config, $user): void
$dirname = trim($_POST['dirname'] ?? '');
$dirname = preg_replace('/[^a-zA-Z0-9._-]/', '-', $dirname);
if ($dirname === '' || $dirname === '.' || $dirname === '..') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige mapnaam.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige mapnaam.') . '&msgtype=danger');
exit;
}
$newPath = $realContentDir . '/' . ($inDir ? $inDir . '/' : '') . $dirname;
if (file_exists($newPath)) {
header('Location: /admin/content-files?msg=' . urlencode('Map bestaat al.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map bestaat al.') . '&msgtype=danger');
exit;
}
if (@mkdir($newPath, 0755, true)) {
adminLog($config, 'info', $user['username'] . ' creëerde content map ' . ($inDir ? $inDir . '/' : '') . $dirname);
header('Location: /admin/content-files?msg=' . urlencode('Map aangemaakt.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map aangemaakt.') . '&msgtype=success');
} else {
header('Location: /admin/content-files?msg=' . urlencode('Map aanmaken mislukt.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map aanmaken mislukt.') . '&msgtype=danger');
}
exit;
}
@@ -1324,7 +1329,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1366,7 +1371,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
$messageType = 'danger';
} elseif (rename($realDir, $newPath)) {
adminLog($config, 'info', $user['username'] . ' hernoemde content map ' . $dirRel . ' naar ' . $newName);
header('Location: /admin/content-files?msg=' . urlencode('Map hernoemd.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map hernoemd.') . '&msgtype=success');
exit;
} else {
$message = 'Hernoemen mislukt.';
@@ -1378,7 +1383,7 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
}
if ($dirRel === '') {
header('Location: /admin/content-files?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
exit;
}
@@ -1403,18 +1408,18 @@ function handleContentDirRenameIn($auth, $config, $twig, $user, $csrf): void
function handleContentDirDeleteIn($auth, $config, $user): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
$contentDir = $config['content_dir'];
$realContentDir = realpath($contentDir);
if (!$realContentDir || !is_dir($realContentDir)) {
header('Location: /admin/content-files');
header('Location: /admin/content');
exit;
}
@@ -1422,19 +1427,19 @@ function handleContentDirDeleteIn($auth, $config, $user): void
$dirRel = ltrim($dirRel, '/');
foreach (explode('/', $dirRel) as $seg) {
if ($seg === '..' || $seg === '.') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
exit;
}
}
if ($dirRel === '') {
header('Location: /admin/content-files?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Ongeldige map.') . '&msgtype=danger');
exit;
}
$fullPath = $realContentDir . '/' . $dirRel;
$realDir = realpath($fullPath);
if (!$realDir || !is_dir($realDir) || strpos($realDir, $realContentDir) !== 0) {
header('Location: /admin/content-files?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map niet gevonden.') . '&msgtype=danger');
exit;
}
@@ -1445,14 +1450,14 @@ function handleContentDirDeleteIn($auth, $config, $user): void
if ($e[0] !== '.') { $isEmpty = false; break; }
}
if (!$isEmpty) {
header('Location: /admin/content-files?msg=' . urlencode('Map moet leeg zijn.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map moet leeg zijn.') . '&msgtype=danger');
exit;
}
if (@rmdir($realDir)) {
adminLog($config, 'info', $user['username'] . ' verwijderde content map ' . $dirRel);
header('Location: /admin/content-files?msg=' . urlencode('Map verwijderd.') . '&msgtype=success');
header('Location: /admin/content?msg=' . urlencode('Map verwijderd.') . '&msgtype=success');
} else {
header('Location: /admin/content-files?msg=' . urlencode('Map verwijderen mislukt.') . '&msgtype=danger');
header('Location: /admin/content?msg=' . urlencode('Map verwijderen mislukt.') . '&msgtype=danger');
}
exit;
}
@@ -1470,7 +1475,7 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
$realPath = realpath($filePath);
$realContentDir = realpath($contentDir);
if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1690,13 +1695,13 @@ function handleContentNew($auth, $config, $twig, $user, $csrf, $siteConfig): voi
function handleContentDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1710,20 +1715,20 @@ function handleContentDelete($auth, $config): void
adminLog($config, 'info', $user['username'] . ' verwijderde ' . $file);
}
header('Location: /admin/content?dir=' . urlencode(dirname($file)));
header('Location: /admin/content-list?dir=' . urlencode(dirname($file)));
exit;
}
function handleContentDirCreate($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1741,7 +1746,7 @@ function handleContentDirCreate($auth, $config): void
}
}
header('Location: /admin/content?dir=' . urlencode($subdir));
header('Location: /admin/content-list?dir=' . urlencode($subdir));
exit;
}
@@ -1768,7 +1773,7 @@ function handleContentDirRename($auth, $config, $twig, $user, $csrf): void
if (!file_exists($newPath) && $newPath !== $fullPath) {
rename($fullPath, $newPath);
adminLog($config, 'info', $user['username'] . ' hernoemde map ' . basename($dir) . ' naar ' . $newName);
header('Location: /admin/content?dir=' . urlencode(dirname($dir) . '/' . $newName));
header('Location: /admin/content-list?dir=' . urlencode(dirname($dir) . '/' . $newName));
exit;
} else {
$message = 'Map bestaat al of ongeldige naam.';
@@ -1797,13 +1802,13 @@ function handleContentDirRename($auth, $config, $twig, $user, $csrf): void
function handleContentDirDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content');
header('Location: /admin/content-list');
exit;
}
@@ -1821,7 +1826,7 @@ function handleContentDirDelete($auth, $config): void
}
}
header('Location: /admin/content?dir=' . urlencode(dirname($dir)));
header('Location: /admin/content-list?dir=' . urlencode(dirname($dir)));
exit;
}
@@ -1867,7 +1872,7 @@ function handleContentMove($auth, $config, $twig, $user, $csrf): void
if (!file_exists($newPath)) {
rename($fullPath, $newPath);
adminLog($config, 'info', $user['username'] . ' verplaatste ' . $item . ' naar ' . $dest);
header('Location: /admin/content?dir=' . urlencode($dest));
header('Location: /admin/content-list?dir=' . urlencode($dest));
exit;
} else {
$message = 'Bestand of map bestaat al op de bestemming.';