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:
+114
-61
@@ -90,8 +90,9 @@ class CodePressCMS {
|
||||
* Removes any characters that are not alphanumeric, dashes, underscores, or slashes
|
||||
*/
|
||||
private function sanitizePageParam(string $page): string {
|
||||
// Remove any characters that could be used for XSS
|
||||
$sanitized = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', $page);
|
||||
// Remove any characters that could be used for XSS; keep the dot so
|
||||
// content file extensions (md/php/html) survive in the URL.
|
||||
$sanitized = preg_replace('/[^a-zA-Z0-9_\-\/.]/', '', $page);
|
||||
return $sanitized ?: 'invalid-page';
|
||||
}
|
||||
|
||||
@@ -330,8 +331,8 @@ class CodePressCMS {
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
// Skip hidden/system entries: dotfiles, -assets, _drafts, etc.
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
// Skip hidden/system entries: dotfiles (.map, .bak) en dash-prefixed (-assets)
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
|
||||
// Skip assets directory (old name, kept for safety)
|
||||
if ($item === 'assets' && is_dir($dir . '/' . $item)) continue;
|
||||
@@ -359,12 +360,13 @@ class CodePressCMS {
|
||||
// Always use filename for navigation (not H1 titles from content)
|
||||
$filename = pathinfo($item, PATHINFO_FILENAME);
|
||||
$title = $this->formatDisplayName($filename);
|
||||
$pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
// Keep the extension in the URL so files sharing the same name
|
||||
// but different type each keep their own viewable link.
|
||||
$result[] = [
|
||||
'type' => 'file',
|
||||
'title' => $title,
|
||||
'path' => $pathWithoutExt,
|
||||
'url' => $this->buildUrl($pathWithoutExt)
|
||||
'path' => $relativePath,
|
||||
'url' => $this->buildUrl($relativePath)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -398,8 +400,8 @@ class CodePressCMS {
|
||||
$items = scandir($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
// Skip hidden/system entries (consistent met scanDirectory)
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
// Skip hidden/system entries: dotfiles (.map, .bak) en dash-prefixed (-assets)
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
@@ -481,8 +483,13 @@ class CodePressCMS {
|
||||
$page = $_GET['page'] ?? $this->getEffectiveDefaultPage();
|
||||
// Limit length
|
||||
$page = substr($page, 0, 255);
|
||||
// Only remove file extension at the end, not all dots
|
||||
$pageWithoutExt = preg_replace('/\.(md|php|html)$/', '', $page);
|
||||
// Detect explicit extension from the URL (e.g. /nl/test.php → use test.php)
|
||||
$explicitExt = null;
|
||||
if (preg_match('/\.(md|php|html)$/i', $page, $extMatch)) {
|
||||
$explicitExt = strtolower($extMatch[1]);
|
||||
}
|
||||
// Strip any content extension from the URL for the base path
|
||||
$pageWithoutExt = preg_replace('/\.(md|php|html)$/i', '', $page);
|
||||
|
||||
$filePath = $this->config['content_dir'] . '/' . $pageWithoutExt;
|
||||
|
||||
@@ -500,43 +507,32 @@ class CodePressCMS {
|
||||
|
||||
$actualFilePath = null;
|
||||
|
||||
// Check for exact file matches if no directory found
|
||||
if (file_exists($filePath . '.md')) {
|
||||
$actualFilePath = $filePath . '.md';
|
||||
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath);
|
||||
} elseif (file_exists($filePath . '.php')) {
|
||||
$actualFilePath = $filePath . '.php';
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif (file_exists($filePath . '.html')) {
|
||||
$actualFilePath = $filePath . '.html';
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
} elseif (file_exists($filePath)) {
|
||||
$actualFilePath = $filePath;
|
||||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'md') {
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath);
|
||||
} elseif ($extension === 'php') {
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif ($extension === 'html') {
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
// If the URL has an explicit extension, try that exact file first
|
||||
if ($explicitExt !== null && file_exists($filePath . '.' . $explicitExt)) {
|
||||
$actualFilePath = $filePath . '.' . $explicitExt;
|
||||
$result = $this->resolveContentByType($filePath, $explicitExt);
|
||||
if ($result !== null) {
|
||||
$actualFilePath = $result['path'];
|
||||
$result = $result['content'];
|
||||
}
|
||||
}
|
||||
// No explicit extension: serve the first matching file (md > php > html)
|
||||
if (!isset($result)) {
|
||||
$resolved = $this->resolveContentByType($filePath);
|
||||
if ($resolved !== null) {
|
||||
$actualFilePath = $resolved['path'];
|
||||
$result = $resolved['content'];
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match found, check for language-specific versions
|
||||
if (!isset($result)) {
|
||||
$result = null; // Reset result before language-specific search
|
||||
$langPrefix = $this->currentLanguage;
|
||||
|
||||
if (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.md')) {
|
||||
$actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.md';
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath);
|
||||
} elseif (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.php')) {
|
||||
$actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.php';
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.html')) {
|
||||
$actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.html';
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
$langBase = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt;
|
||||
$resolved = $this->resolveContentByType($langBase, $explicitExt);
|
||||
if ($resolved !== null) {
|
||||
$actualFilePath = $resolved['path'];
|
||||
$result = $resolved['content'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +549,40 @@ class CodePressCMS {
|
||||
return $this->getError404();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the first content file matching a base path by type priority.
|
||||
*
|
||||
* A request without explicit extension (e.g. "test") serves the first
|
||||
* existing file among test.md → test.php → test.html. When $preferredExt
|
||||
* is given (the URL carried an extension), that type is tried first so
|
||||
* /nl/test.php keeps serving test.php even when test.md also exists.
|
||||
*
|
||||
* @param string $basePath Absolute path without extension
|
||||
* @param string $preferredExt Optional extension to try first (md|php|html)
|
||||
* @return array|null ['path' => string, 'content' => array] or null
|
||||
*/
|
||||
private function resolveContentByType(string $basePath, ?string $preferredExt = null): ?array
|
||||
{
|
||||
$order = ['md', 'php', 'html'];
|
||||
if ($preferredExt !== null && in_array($preferredExt, $order, true)) {
|
||||
$order = array_unique(array_merge([$preferredExt], $order));
|
||||
}
|
||||
foreach ($order as $ext) {
|
||||
$candidate = $basePath . '.' . $ext;
|
||||
if (file_exists($candidate)) {
|
||||
if ($ext === 'md') {
|
||||
$content = $this->parseMarkdown(file_get_contents($candidate), $candidate);
|
||||
} elseif ($ext === 'php') {
|
||||
$content = $this->parsePHP($candidate);
|
||||
} else {
|
||||
$content = $this->parseHTML(file_get_contents($candidate));
|
||||
}
|
||||
return ['path' => $candidate, 'content' => $content];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file information including creation and modification dates
|
||||
*
|
||||
@@ -836,7 +866,11 @@ class CodePressCMS {
|
||||
private function autoLinkPageTitles($content, $excludeTitle = '') {
|
||||
$pages = $this->getAllPageTitles();
|
||||
|
||||
foreach ($pages as $pagePath => $pageTitle) {
|
||||
foreach ($pages as $page) {
|
||||
$pageTitle = $page['title'];
|
||||
$pagePath = $page['path'];
|
||||
// Alleen bestanden auto-linken, geen mappen
|
||||
if ($page['type'] === 'folder') continue;
|
||||
if (strtolower($pageTitle) === strtolower($excludeTitle)) {
|
||||
continue;
|
||||
}
|
||||
@@ -863,9 +897,10 @@ class CodePressCMS {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all page titles from content directory
|
||||
* Get all content entries (mappen + bestanden) from the content directory.
|
||||
*
|
||||
* @return array Associative array of page paths to titles
|
||||
* @return array List of entries: ['path' => ..., 'title' => ..., 'type' => ...]
|
||||
* type is 'md'/'php'/'html' voor bestanden, 'folder' voor mappen.
|
||||
*/
|
||||
public function getAllPageTitles() {
|
||||
$pages = [];
|
||||
@@ -874,7 +909,13 @@ class CodePressCMS {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan for page titles in directory
|
||||
* Recursively scan for content entries in a directory.
|
||||
*
|
||||
* Returns a flat list of ['path' => relativePath, 'title' => ..., 'type' => ...].
|
||||
* Mappen krijgen type 'folder'; bestanden krijgen hun extensie als type.
|
||||
* Bestanden met dezelfde naam maar ander type blijven apart (de extensie
|
||||
* zit in het pad), en mappen krijgen hun eigen entry zodat de boom zichtbaar
|
||||
* blijft.
|
||||
*
|
||||
* @param string $dir Directory to scan
|
||||
* @param string $prefix Relative path prefix
|
||||
@@ -888,26 +929,30 @@ class CodePressCMS {
|
||||
sort($items);
|
||||
|
||||
foreach ($items as $item) {
|
||||
// Skip hidden/system entries (consistent met scanDirectory)
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
// Skip hidden/system entries: dotfiles (.map, .bak) en dash-prefixed (-assets)
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$pages[] = [
|
||||
'path' => $relativePath,
|
||||
'title' => $this->formatDisplayName($item),
|
||||
'type' => 'folder',
|
||||
];
|
||||
$this->scanForPageTitles($path, $relativePath, $pages);
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
$title = $this->extractPageTitle($path);
|
||||
if ($title && !empty(trim($title))) {
|
||||
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
$pages[$pagePath] = $title;
|
||||
} else {
|
||||
// Fallback to clean filename if no title found in content
|
||||
if (!$title || empty(trim($title))) {
|
||||
$filename = basename($path, pathinfo($path, PATHINFO_EXTENSION));
|
||||
$cleanName = $this->formatDisplayName($filename);
|
||||
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
$pages[$pagePath] = $cleanName;
|
||||
$title = $this->formatDisplayName($filename);
|
||||
}
|
||||
$pages[] = [
|
||||
'path' => $relativePath,
|
||||
'title' => $title,
|
||||
'type' => pathinfo($item, PATHINFO_EXTENSION),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1002,9 +1047,15 @@ class CodePressCMS {
|
||||
// Make API and metadata available to the included file
|
||||
$api = new ContentAPI($this);
|
||||
$pageMetadata = $metadata;
|
||||
include $filePath;
|
||||
$returnedContent = include $filePath;
|
||||
$content = ob_get_clean();
|
||||
|
||||
// If the PHP file returned a string, use that as content (callback style).
|
||||
// Otherwise use the buffered echo output.
|
||||
if (is_string($returnedContent) && $returnedContent !== '') {
|
||||
$content = $returnedContent;
|
||||
}
|
||||
|
||||
// Remove any remaining metadata from PHP output
|
||||
$content = preg_replace('/^---\s*\n.*?\n---\s*\n/s', '', $content);
|
||||
|
||||
@@ -1232,7 +1283,8 @@ class CodePressCMS {
|
||||
$allItems = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
// Skip hidden/system entries: dotfiles (.map, .bak) en dash-prefixed (-assets)
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
|
||||
$itemPath = $dirPath . '/' . $item;
|
||||
$relativePath = $pagePath ? $pagePath . '/' . $item : $item;
|
||||
@@ -1246,15 +1298,16 @@ class CodePressCMS {
|
||||
'type' => 'directory'
|
||||
];
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
// Each file keeps its own link (incl. extension) so every type
|
||||
// stays viewable at the frontend even when names overlap.
|
||||
$extractedTitle = $this->extractPageTitle($itemPath);
|
||||
$fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
||||
$pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
$icon = pathinfo($item, PATHINFO_EXTENSION) === 'md' ? 'bi-file-text' :
|
||||
(pathinfo($item, PATHINFO_EXTENSION) === 'php' ? 'bi-file-code' : 'bi-file-earmark');
|
||||
$allItems[] = [
|
||||
'name' => $fileTitle,
|
||||
'path' => $pathWithoutExt,
|
||||
'url' => $this->buildUrl($pathWithoutExt),
|
||||
'path' => $relativePath,
|
||||
'url' => $this->buildUrl($relativePath),
|
||||
'icon' => $icon,
|
||||
'type' => 'file'
|
||||
];
|
||||
|
||||
@@ -17,9 +17,10 @@ class ContentAPI
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages as a flat array of path => title pairs
|
||||
* Get all content entries (mappen + bestanden) as a list of entry arrays.
|
||||
*
|
||||
* @return array Associative array like ['index' => 'Home', 'over-ons' => 'Over ons']
|
||||
* @return array List of ['path' => ..., 'title' => ..., 'type' => ...]
|
||||
* type is 'md'/'php'/'html' voor bestanden, 'folder' voor mappen.
|
||||
*/
|
||||
public function getAllPages(): array
|
||||
{
|
||||
@@ -29,18 +30,32 @@ class ContentAPI
|
||||
/**
|
||||
* Get a single page by path, including title, content, layout, and metadata
|
||||
*
|
||||
* @param string $path Page path without extension (e.g. 'over-ons' or 'blog/post-1')
|
||||
* De $path mag een extensie bevten (bijv. 'over-ons.php') of niet ('over-ons').
|
||||
* Zonder extensie wordt het eerste bestaande bestand gekozen (md > php > html);
|
||||
* met extensie wordt dat specifieke bestand gekozen.
|
||||
*
|
||||
* @param string $path Page path, al dan niet met extensie
|
||||
* @return array|null Page data or null if not found
|
||||
*/
|
||||
public function getPage(string $path): ?array
|
||||
{
|
||||
$contentDir = $this->cms->config['content_dir'];
|
||||
$path = preg_replace('/\.(md|php|html)$/', '', $path);
|
||||
|
||||
// Detecteer expliciete extensie en strip hem voor de basis
|
||||
$preferredExt = null;
|
||||
if (preg_match('/\.(md|php|html)$/i', $path, $m)) {
|
||||
$preferredExt = strtolower($m[1]);
|
||||
}
|
||||
$path = preg_replace('/\.(md|php|html)$/i', '', $path);
|
||||
$filePath = $contentDir . '/' . $path;
|
||||
|
||||
$extensions = ['md', 'php', 'html'];
|
||||
// Probeer de gevraagde extensie eerst, dan de standaard volgorde
|
||||
$order = ['md', 'php', 'html'];
|
||||
if ($preferredExt !== null) {
|
||||
$order = array_unique(array_merge([$preferredExt], $order));
|
||||
}
|
||||
$actualPath = null;
|
||||
foreach ($extensions as $ext) {
|
||||
foreach ($order as $ext) {
|
||||
if (file_exists($filePath . '.' . $ext)) {
|
||||
$actualPath = $filePath . '.' . $ext;
|
||||
break;
|
||||
@@ -141,14 +156,13 @@ class ContentAPI
|
||||
/**
|
||||
* Check if a page exists at the given path
|
||||
*
|
||||
* @param string $path Page path without extension
|
||||
* @param string $path Page path, al dan niet met extensie
|
||||
* @return bool
|
||||
*/
|
||||
public function pageExists(string $path): bool
|
||||
{
|
||||
$contentDir = $this->cms->config['content_dir'];
|
||||
$path = preg_replace('/\.(md|php|html)$/', '', $path);
|
||||
$basePath = $contentDir . '/' . $path;
|
||||
$basePath = $contentDir . '/' . preg_replace('/\.(md|php|html)$/i', '', $path);
|
||||
|
||||
return file_exists($basePath . '.md')
|
||||
|| file_exists($basePath . '.php')
|
||||
|
||||
@@ -224,7 +224,9 @@ class CMSAPI implements PluginAPIInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages with their metadata
|
||||
* Get all content entries (mappen + bestanden) as a list of entry arrays.
|
||||
*
|
||||
* @return array List of ['path' => ..., 'title' => ..., 'type' => ...]
|
||||
*/
|
||||
public function getAllPages(): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user