v2.6.3 (Lyra): Content multi-type handling, getAllPages() structuur, . verberg-prefix

- Content bestanden met dezelfde naam maar ander type (md/php/html) worden
  correct geserveerd: URL met extensie opent dat bestand, URL zonder extensie
  valt terug op md > php > html (resolveContentByType helper)
- Admin content editor accepteert bestanden met dezelfde naam (ander type);
  preview-knop linkt per extensie
- Frontend navigatie/directory listing/search tonen elk bestandstype apart
- getAllPages() array structuur gewijzigd naar list van
  ['path','title','type'] met type 'md'/'php'/'html'/'folder'
- Verberg-prefix logica: _ is geen verberg-prefix meer, alleen . (en -);
  admin toont wél alle . bestanden/mappen
- ContentAPI getPage()/pageExists() respecteren expliciete extensie
- Handleiding content-api.md (NL+EN) herschreven
- File-tree unificatie: _file-tree.twig + _editor-styles.twig includes
- Versie verhoogd naar 2.6.3
This commit is contained in:
2026-08-20 16:45:53 +00:00
parent 97c4d52c78
commit 6485f693dc
28 changed files with 1481 additions and 950 deletions
+203 -108
View File
@@ -159,6 +159,59 @@ function isProtectedPlugin(string $pluginName): bool {
return in_array($pluginName, getProtectedPlugins(), true);
}
/**
* Get the list of content-type plugins (name => [name, title, type]).
* Used by the content editor "visible plugins" selector so only content
* plugins (not system plugins like Statistics/Logs/Dashboard) are offered.
*
* Type resolution mirrors handlePlugins:
* 1. default 'content'
* 2. plugin.json 'type' override
* 3. regex from <Plugin>.php 'type' => '...' (fallback when plugin.json has no type)
*
* @param string $pluginsDir Absolute path to the plugins directory
* @return array<int,array{name:string,title:string,type:string}> Content-type plugins only
*/
function getContentPlugins(string $pluginsDir): array
{
$plugins = [];
if (!is_dir($pluginsDir)) {
return $plugins;
}
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) {
$pluginName = basename($pluginDir);
if ($pluginName === '.' || $pluginName === '..') continue;
$pluginJson = $pluginDir . '/plugin.json';
$type = 'content';
$title = ucfirst($pluginName);
if (file_exists($pluginJson)) {
$data = json_decode(file_get_contents($pluginJson), true);
if (is_array($data)) {
if (isset($data['type'])) $type = $data['type'];
if (isset($data['name'])) $title = $data['name'];
elseif (isset($data['title'])) $title = $data['title'];
}
}
if ($type === 'content') {
// Fallback: detect type from plugin PHP when plugin.json has no explicit type
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
if (file_exists($pluginJson) && !isset($data['type']) && file_exists($pluginFile)) {
$source = file_get_contents($pluginFile);
if (preg_match("/'type'\s*=>\s*'([^']+)'/", $source, $m)) {
$type = $m[1];
}
}
}
if ($type === 'content') {
$plugins[] = ['name' => $pluginName, 'title' => $title, 'type' => $type];
}
}
usort($plugins, function ($a, $b) { return strcasecmp($a['name'], $b['name']); });
return $plugins;
}
// Public routes (no auth required)
if ($route === 'login') {
$error = '';
@@ -274,8 +327,9 @@ switch ($route) {
break;
case 'content-list':
handleContent($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
// Lijst weergave is verwijderd; redirect naar de boom-editor
header('Location: /admin/content');
exit;
case 'content-files':
// Backward compat: redirect to the unified content editor
@@ -737,13 +791,21 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
} else {
$newReal = $parentReal . '/' . basename($newFullPath);
if (file_exists($newReal)) {
$message = 'Bestand bestaat al.';
$message = 'Bestand met dit type bestaat al.';
$messageType = 'danger';
} else {
// Build frontmatter with author metadata (zoals handleContentNew)
// Build frontmatter with layout/created/edited timestamps.
// All editable types (md/php/html) support --- frontmatter; the CMS
// parseMetadata() extracts it and parsePHP() strips it from output.
$now = date('Y-m-d H:i:s');
$frontmatter = "---\nlayout: full_content\nauthor_name: " . ($user['author_name'] ?? $user['username'] ?? '') . "\nauthor_email: " . ($user['author_email'] ?? '') . "\ncreated: " . $now . "\n---\n\n";
$stub = $ext === 'md' ? $frontmatter . "# Nieuwe pagina\n\n" : ($ext === 'php' ? "<?php\n" : "<h1>Nieuwe pagina</h1>\n");
$frontmatter = "---\nlayout: full_content\ncreated: " . $now . "\nedited: " . $now . "\n---\n\n";
if ($ext === 'md') {
$stub = $frontmatter . "# Nieuwe pagina\n\n";
} elseif ($ext === 'php') {
$stub = $frontmatter . "<?php\n";
} else {
$stub = $frontmatter . "<h1>Nieuwe pagina</h1>\n";
}
$written = @file_put_contents($newReal, $stub);
if ($written !== false) {
adminLog($config, 'info', $user['username'] . ' creëerde content bestand ' . $newRel);
@@ -870,23 +932,57 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
// Save the currently selected file
$content = $_POST['content'] ?? '';
$layout = $_POST['layout'] ?? '';
$newFilename = trim($_POST['new_filename'] ?? '');
if ($realPath && is_file($realPath)) {
$ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
if (in_array($ext, $editableExts, true)) {
if ($layout && $ext === 'md') {
if ($layout) {
$content = updateContentFrontmatter($content, 'layout', $layout);
}
$plugins = isset($_POST['plugins']) && is_array($_POST['plugins'])
? implode(', ', array_map('trim', $_POST['plugins']))
: '';
if ($ext === 'md') {
$content = updateContentFrontmatter($content, 'plugins', $plugins);
$content = updateContentFrontmatter($content, 'plugins', $plugins);
// Stamp the last edit time so the editor can show created/edited
$content = updateContentFrontmatter($content, 'edited', date('Y-m-d H:i:s'));
// Rename if the filename changed
if ($newFilename !== '') {
$cleanName = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename);
$cleanName = preg_replace('/\.(md|php|html)$/i', '', $cleanName);
$newFull = dirname($realPath) . '/' . $cleanName . '.' . $ext;
if ($newFull !== $realPath) {
if (file_exists($newFull)) {
$message = 'Bestandsnaam bestaat al.';
$messageType = 'danger';
} else {
backupContentFile($realPath);
file_put_contents($realPath, $content);
rename($realPath, $newFull);
$realPath = $newFull;
$relFile = trim(dirname($relFile), '.\\/') ? trim(dirname($relFile), '.\\/') . '/' . $cleanName . '.' . $ext : $cleanName . '.' . $ext;
$renamedTo = $relFile;
adminLog($config, 'info', $user['username'] . ' hernoemde content naar ' . $relFile);
$message = 'Bestand opgeslagen en hernoemd.';
$messageType = 'success';
}
}
}
backupContentFile($realPath);
file_put_contents($realPath, $content);
adminLog($config, 'info', $user['username'] . ' bewerkte content ' . $relFile);
$message = 'Bestand opgeslagen.';
$messageType = 'success';
if ($message !== 'Bestandsnaam bestaat al.') {
backupContentFile($realPath);
file_put_contents($realPath, $content);
if ($message === '') {
adminLog($config, 'info', $user['username'] . ' bewerkte content ' . $relFile);
$message = 'Bestand opgeslagen.';
$messageType = 'success';
}
// If renamed, redirect to the new URL so the browser URL stays in sync
if (isset($renamedTo)) {
header('Location: /admin/content?file=' . urlencode($renamedTo) . '&msg=' . urlencode($message) . '&msgtype=success');
exit;
}
}
} else {
$message = 'Dit bestandstype kan niet bewerkt worden.';
$messageType = 'danger';
@@ -917,9 +1013,11 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
$fileContent = '';
$fileExt = '';
$fileName = '';
$fileBaseName = '';
if ($realPath && is_file($realPath)) {
$fileExt = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
$fileName = basename($realPath);
$fileBaseName = basename($realPath, '.' . $fileExt);
if (in_array($fileExt, $editableExts, true)) {
$isEditable = true;
$fileContent = file_get_contents($realPath);
@@ -943,15 +1041,17 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
}
}
// Available plugins for the plugins checkbox group
// Available content plugins for the plugins multiselect (only active
// content-type plugins, never system plugins like Statistics/Logs).
$pluginsDir = $config['plugins_dir'];
$contentPlugins = getContentPlugins($pluginsDir);
// Keep only enabled plugins so the selector reflects what actually runs.
// enabled_plugins lives in config.json ($siteConfig), not in app.php.
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
$availablePlugins = [];
if (is_dir($pluginsDir)) {
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) {
$pluginName = basename($pluginDir);
if ($pluginName !== '.' && $pluginName !== '..' && file_exists($pluginDir . '/plugin.json')) {
$availablePlugins[] = $pluginName;
}
foreach ($contentPlugins as $p) {
if (in_array($p['name'], $enabledPlugins, true)) {
$availablePlugins[] = $p;
}
}
@@ -960,26 +1060,11 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
$currentPlugins = $isEditable ? extractFrontmatterValue($fileContent, 'plugins') : '';
$selectedPlugins = array_map('trim', explode(',', $currentPlugins));
if ($selectedPlugins === ['']) $selectedPlugins = [];
$currentAuthorName = $isEditable ? extractFrontmatterValue($fileContent, 'author_name') : '';
$currentAuthorEmail = $isEditable ? extractFrontmatterValue($fileContent, 'author_email') : '';
$currentCreated = $isEditable ? extractFrontmatterValue($fileContent, 'created') : '';
$currentEdited = $isEditable ? extractFrontmatterValue($fileContent, 'edited') : '';
$currentLang = $relFile ? extractLanguagePrefix($relFile) : 'nl';
$fileDir = $relFile ? trim(dirname($relFile), '.\\/') : '';
// Git status for Fase 5 (content git integratie in zijbalk)
$gitStatus = ['available' => false, 'initialized' => false, 'dirty' => false, 'branch' => '', 'ahead' => 0, 'behind' => 0, 'lastCommit' => ''];
$gitDir = $realContentDir . '/.git';
$gitStatus['available'] = $is_dir = is_dir($gitDir);
if ($gitStatus['available']) {
$gitStatus['initialized'] = true;
$branch = @shell_exec('cd ' . escapeshellarg($realContentDir) . ' && git rev-parse --abbrev-ref HEAD 2>/dev/null');
$gitStatus['branch'] = trim($branch ?? '');
$dirty = @shell_exec('cd ' . escapeshellarg($realContentDir) . ' && git status --porcelain 2>/dev/null');
$gitStatus['dirty'] = trim($dirty ?? '') !== '';
$lastCommit = @shell_exec('cd ' . escapeshellarg($realContentDir) . ' && git log -1 --format="%h %s (%cr)" 2>/dev/null');
$gitStatus['lastCommit'] = trim($lastCommit ?? '');
}
$route = 'content-files';
echo $twig->render('pages/content-files.twig', [
@@ -988,7 +1073,9 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
'csrf_token' => $csrf,
'relFile' => $relFile,
'fileDir' => $fileDir,
'selectedDir' => $fileDir,
'fileName' => $fileName,
'fileBaseName' => $fileBaseName,
'fileExt' => $fileExt,
'fileContent' => $fileContent,
'isEditable' => $isEditable,
@@ -1001,10 +1088,8 @@ function handleContentFiles($auth, $config, $twig, $user, $csrf, $siteConfig): v
'availablePlugins' => $availablePlugins,
'selectedPlugins' => $selectedPlugins,
'currentLang' => $currentLang,
'currentAuthorName' => $currentAuthorName,
'currentAuthorEmail' => $currentAuthorEmail,
'currentCreated' => $currentCreated,
'gitStatus' => $gitStatus,
'currentEdited' => $currentEdited,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => $isEditable,
'message' => $message,
@@ -1232,6 +1317,16 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
break;
}
}
// Optional rename: new filename (without extension, extension is preserved)
$newName = trim($_POST['new_name'] ?? '');
$ext = strtolower(pathinfo($relFile, PATHINFO_EXTENSION));
if ($newName !== '') {
$newName = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newName);
// Strip any extension the user may have typed (we preserve the original)
$newName = preg_replace('/\.(md|php|html)$/i', '', $newName);
} else {
$newName = basename($relFile, '.' . $ext);
}
if ($message === '') {
$destDir = $dest === '' ? $realContentDir : $realContentDir . '/' . $dest;
$realDestDir = realpath($destDir);
@@ -1239,18 +1334,24 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
$message = 'Doelmap bestaat niet.';
$messageType = 'danger';
} else {
$newPath = $realDestDir . '/' . basename($realPath);
$newFileName = $newName . '.' . $ext;
$newPath = $realDestDir . '/' . $newFileName;
if (strtolower($newPath) === strtolower($realPath)) {
// No change
header('Location: /admin/content?file=' . urlencode($relFile) . '&msg=' . urlencode('Geen wijziging.') . '&msgtype=info');
exit;
}
if (file_exists($newPath)) {
$message = 'Bestand bestaat al op de bestemming.';
$messageType = 'danger';
} 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?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand verplaatst.') . '&msgtype=success');
$newRel = $dest === '' ? $newFileName : $dest . '/' . $newFileName;
adminLog($config, 'info', $user['username'] . ' hernoemde/verplaatste content/' . $relFile . ' naar content/' . $newRel);
header('Location: /admin/content?file=' . urlencode($newRel) . '&msg=' . urlencode('Bestand hernoemd/verplaatst.') . '&msgtype=success');
exit;
} else {
$err = error_get_last();
$message = 'Verplaatsen mislukt: ' . ($err['message'] ?? 'onbekend');
$message = 'Hernoemen/verplaatsen mislukt: ' . ($err['message'] ?? 'onbekend');
$messageType = 'danger';
}
}
@@ -1263,7 +1364,8 @@ function handleContentFileMove($auth, $config, $twig, $user, $csrf): void
'route' => 'content-file-move',
'csrf_token' => $csrf,
'relFile' => $relFile,
'itemName' => basename($relFile),
'itemName' => basename($relFile, '.' . pathinfo($relFile, PATHINFO_EXTENSION)),
'itemExt' => '.' . pathinfo($relFile, PATHINFO_EXTENSION),
'itemDir' => trim(dirname($relFile), '.\\/'),
'directories' => $directories,
'sidebar_color' => getSidebarColor($config),
@@ -1475,7 +1577,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-list');
header('Location: /admin/content');
exit;
}
@@ -1487,28 +1589,6 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
$message = 'Ongeldige CSRF token.';
$messageType = 'danger';
} else {
// Handle rename
$newFilename = trim($_POST['filename'] ?? '');
$wasRenamed = false;
if (!empty($newFilename)) {
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename);
$newFilename .= '.' . $fileExt;
$parentDir = dirname($filePath);
$newFilePath = $parentDir . '/' . $newFilename;
$realParentDir = realpath($parentDir);
if ($realParentDir && strpos($realParentDir, $realContentDir) === 0) {
if ($newFilePath !== $filePath && !file_exists($newFilePath)) {
rename($filePath, $newFilePath);
$wasRenamed = true;
adminLog($config, 'info', $user['username'] . ' hernoemde ' . basename($filePath) . ' naar ' . $newFilename);
$filePath = $newFilePath;
$newFile = dirname($file) . '/' . $newFilename;
$file = ltrim($newFile, './');
}
}
}
// Save content only for editable files
if ($isEditable) {
$content = $_POST['content'] ?? '';
@@ -1520,11 +1600,10 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
? implode(', ', array_map('trim', $_POST['plugins']))
: '';
$content = updateContentFrontmatter($content, 'plugins', $plugins);
$content = updateContentFrontmatter($content, 'edited', date('Y-m-d H:i:s'));
backupContentFile($filePath);
file_put_contents($filePath, $content);
if (!$wasRenamed) {
adminLog($config, 'info', $user['username'] . ' bewerkte ' . basename($filePath));
}
adminLog($config, 'info', $user['username'] . ' bewerkte ' . basename($filePath));
$message = 'Bestand opgeslagen.';
$messageType = 'success';
}
@@ -1532,12 +1611,12 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
}
$fileName = basename($filePath);
$fileBaseName = basename($filePath, '.' . $fileExt);
$fileContent = $isEditable ? file_get_contents($filePath) : '';
$currentLayout = extractFrontmatterValue($fileContent, 'layout');
$currentPlugins = extractFrontmatterValue($fileContent, 'plugins');
$currentAuthorName = extractFrontmatterValue($fileContent, 'author_name');
$currentAuthorEmail = extractFrontmatterValue($fileContent, 'author_email');
$currentCreated = extractFrontmatterValue($fileContent, 'created');
$currentEdited = extractFrontmatterValue($fileContent, 'edited');
$selectedPlugins = array_map('trim', explode(',', $currentPlugins));
if ($selectedPlugins === ['']) $selectedPlugins = [];
@@ -1569,15 +1648,15 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
}
}
// Get available plugins
// Get available content plugins (only enabled content-type plugins).
// enabled_plugins lives in config.json ($siteConfig), not in app.php.
$pluginsDir = $config['plugins_dir'];
$contentPlugins = getContentPlugins($pluginsDir);
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
$availablePlugins = [];
if (is_dir($pluginsDir)) {
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) {
$pluginName = basename($pluginDir);
if ($pluginName !== '.' && $pluginName !== '..' && file_exists($pluginDir . '/plugin.json')) {
$availablePlugins[] = $pluginName;
}
foreach ($contentPlugins as $p) {
if (in_array($p['name'], $enabledPlugins, true)) {
$availablePlugins[] = $p;
}
}
@@ -1592,6 +1671,7 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
'file' => $file,
'fileDir' => $fileDir,
'fileName' => $fileName,
'fileBaseName' => $fileBaseName,
'fileExt' => $fileExt,
'fileContent' => $fileContent,
'isEditable' => $isEditable,
@@ -1602,9 +1682,8 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
'availablePlugins' => $availablePlugins,
'selectedPlugins' => $selectedPlugins,
'currentLang' => $currentLang,
'currentAuthorName' => $currentAuthorName,
'currentAuthorEmail' => $currentAuthorEmail,
'currentCreated' => $currentCreated,
'currentEdited' => $currentEdited,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => $isEditable,
'message' => $message,
@@ -1645,28 +1724,35 @@ function handleContentNew($auth, $config, $twig, $user, $csrf, $siteConfig): voi
$messageType = 'danger';
} else {
$filename = trim($_POST['filename'] ?? '');
$ext = $_POST['extension'] ?? 'md';
$ext = strtolower(trim($_POST['extension'] ?? 'md'));
if (!in_array($ext, ['md', 'php', 'html'], true)) $ext = 'md';
$layout = $_POST['layout'] ?? $themeDefaultLayout;
if (!empty($filename)) {
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $filename);
$filename .= '.' . $ext;
$dest = rtrim($fullPath, '/') . '/' . $filename;
if (!file_exists($dest)) {
// Build frontmatter with layout and author metadata
// Build frontmatter with layout and created/edited timestamps.
// All editable types (md/php/html) support --- frontmatter.
$now = date('Y-m-d H:i:s');
$frontmatter = "---\n";
$frontmatter .= "layout: " . $layout . "\n";
$frontmatter .= "author_name: " . ($user['author_name'] ?? $user['username'] ?? '') . "\n";
$frontmatter .= "author_email: " . ($user['author_email'] ?? '') . "\n";
$frontmatter .= "created: " . $now . "\n";
$frontmatter .= "edited: " . $now . "\n";
$frontmatter .= "---\n\n";
$content = $frontmatter . "# Nieuwe pagina\n\n";
if ($ext === 'php') {
$content = $frontmatter . "<?php\n";
} elseif ($ext === 'html') {
$content = $frontmatter . "<h1>Nieuwe pagina</h1>\n";
} else {
$content = $frontmatter . "# Nieuwe pagina\n\n";
}
file_put_contents($dest, $content);
adminLog($config, 'info', $user['username'] . ' creëerde ' . $filename);
header('Location: /admin/content-edit?file=' . urlencode($dir . '/' . $filename));
exit;
} else {
$message = 'Bestand bestaat al.';
$message = 'Bestand met dit type bestaat al.';
$messageType = 'danger';
}
}
@@ -1695,13 +1781,13 @@ function handleContentNew($auth, $config, $twig, $user, $csrf, $siteConfig): voi
function handleContentDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
@@ -1715,20 +1801,20 @@ function handleContentDelete($auth, $config): void
adminLog($config, 'info', $user['username'] . ' verwijderde ' . $file);
}
header('Location: /admin/content-list?dir=' . urlencode(dirname($file)));
header('Location: /admin/content?dir=' . urlencode(dirname($file)));
exit;
}
function handleContentDirCreate($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
@@ -1746,7 +1832,7 @@ function handleContentDirCreate($auth, $config): void
}
}
header('Location: /admin/content-list?dir=' . urlencode($subdir));
header('Location: /admin/content?dir=' . urlencode($subdir));
exit;
}
@@ -1773,7 +1859,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-list?dir=' . urlencode(dirname($dir) . '/' . $newName));
header('Location: /admin/content?dir=' . urlencode(dirname($dir) . '/' . $newName));
exit;
} else {
$message = 'Map bestaat al of ongeldige naam.';
@@ -1802,13 +1888,13 @@ function handleContentDirRename($auth, $config, $twig, $user, $csrf): void
function handleContentDirDelete($auth, $config): void
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
$user = $auth->getCurrentUser();
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
header('Location: /admin/content-list');
header('Location: /admin/content');
exit;
}
@@ -1826,7 +1912,7 @@ function handleContentDirDelete($auth, $config): void
}
}
header('Location: /admin/content-list?dir=' . urlencode(dirname($dir)));
header('Location: /admin/content?dir=' . urlencode(dirname($dir)));
exit;
}
@@ -1872,7 +1958,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-list?dir=' . urlencode($dest));
header('Location: /admin/content?dir=' . urlencode($dest));
exit;
} else {
$message = 'Bestand of map bestaat al op de bestemming.';
@@ -4168,7 +4254,9 @@ function handleMediaList($auth, $config): void
$urlPrefix = '/themes/' . $theme . '/assets/';
} else {
$baseDir = $config['content_dir'];
$urlPrefix = '/content/';
// Serve content media through the /-media/ gateway; /content/ is
// blocked (403) by public/index.php and lives outside the webroot.
$urlPrefix = '/-media/';
}
$realBase = realpath($baseDir);
@@ -4311,7 +4399,7 @@ function scanContentDir(string $fullPath, string $subdir): array
$files = scandir($fullPath);
foreach ($files as $file) {
if ($file === '.' || $file === '..' || str_starts_with($file, '.')) {
if ($file === '.' || $file === '..') {
continue;
}
@@ -4372,7 +4460,7 @@ function scanEditorFiles(string $dir, string $scope = 'plugin'): array
if ($scope === 'theme') {
$skipPaths['assets/css_compiled'] = true;
}
return scanEditorFilesNode($realBase, '', $realBase, $skipPaths);
return scanEditorFilesNode($realBase, '', $realBase, $skipPaths, $scope);
}
/**
@@ -4382,8 +4470,9 @@ function scanEditorFiles(string $dir, string $scope = 'plugin'): array
* @param string $relPath Relative path of $absDir within the base (empty for root)
* @param string $realBase The real base root (for path-traversal guard)
* @param array $skipPaths Map of relative paths to skip (e.g. ['assets/css_compiled' => true])
* @param string $scope 'plugin' | 'theme' | 'content' — content toont . bestanden/mappen
*/
function scanEditorFilesNode(string $absDir, string $relPath, string $realBase, array $skipPaths = []): array
function scanEditorFilesNode(string $absDir, string $relPath, string $realBase, array $skipPaths = [], string $scope = 'plugin'): array
{
$nodes = [];
if (!is_dir($absDir)) {
@@ -4397,9 +4486,15 @@ function scanEditorFilesNode(string $absDir, string $relPath, string $realBase,
if ($entry === '.' || $entry === '..') {
continue;
}
// Skip hidden / dotfiles (incl. .gitkeep, .mtime, .bak, .git)
// Plugin/theme scope: skip dotfiles (.gitkeep, .mtime, .bak, .git)
// Content scope: toon . bestanden/mappen (bijv. .map), maar sla .git/.gitkeep over
if ($entry !== '' && $entry[0] === '.') {
continue;
if ($scope !== 'content') {
continue;
}
if ($entry === '.git' || $entry === '.gitkeep') {
continue;
}
}
$childRelPath = $relPath === '' ? $entry : ($relPath . '/' . $entry);
@@ -4428,7 +4523,7 @@ function scanEditorFilesNode(string $absDir, string $relPath, string $realBase,
];
if ($isDir) {
$node['children'] = scanEditorFilesNode($realChild, $childRelPath, $realBase, $skipPaths);
$node['children'] = scanEditorFilesNode($realChild, $childRelPath, $realBase, $skipPaths, $scope);
}
$nodes[] = $node;
@@ -4500,7 +4595,7 @@ function collectContentPages(string $contentDir): array
// Skip hidden / dash-prefixed segments (private assets etc.)
$skip = false;
foreach (explode('/', $relative) as $segment) {
if ($segment !== '' && ($segment[0] === '.' || $segment[0] === '-')) {
if ($segment !== '' && $segment[0] === '-') {
$skip = true;
break;
}