Merge development into main - Admin console, security fixes, sidebar toggle

# Conflicts:
#	engine/templates/layout.mustache
#	public/assets/js/app.js
This commit is contained in:
2026-02-16 17:16:01 +01:00
135 changed files with 14472 additions and 235 deletions

View File

@@ -45,9 +45,7 @@ class CodePressCMS {
$this->currentLanguage = $this->getCurrentLanguage();
$this->translations = $this->loadTranslations($this->currentLanguage);
// Initialize plugin manager
require_once __DIR__ . '/../plugin/PluginManager.php';
require_once __DIR__ . '/../plugin/CMSAPI.php';
// Initialize plugin manager (files already loaded in engine/core/index.php)
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins');
$api = new CMSAPI($this);
$this->pluginManager->setAPI($api);
@@ -187,10 +185,10 @@ class CodePressCMS {
if ($item[0] === '.') continue;
// Skip language-specific content that doesn't match current language
if (preg_match('/^(nl|en)\./', $item)) {
$langPrefix = substr($item, 0, 2);
if (($langPrefix === 'nl' && $this->currentLanguage !== 'nl') ||
($langPrefix === 'en' && $this->currentLanguage !== 'en')) {
$availableLangs = array_keys($this->getAvailableLanguages());
$langPattern = '/^(' . implode('|', $availableLangs) . ')\./';
if (preg_match($langPattern, $item, $langMatch)) {
if ($langMatch[1] !== $this->currentLanguage) {
continue;
}
}
@@ -261,7 +259,7 @@ class CodePressCMS {
$this->searchResults[] = [
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath,
'url' => '?page=' . $relativePath . '&lang=' . $this->currentLanguage,
'snippet' => $this->createSnippet($content, $query)
];
}
@@ -307,10 +305,6 @@ class CodePressCMS {
}
$page = $_GET['page'] ?? $this->config['default_page'];
// Sanitize page parameter to prevent XSS
$page = htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
// Prevent path traversal
$page = str_replace(['../', '..\\', '..'], '', $page);
// Limit length
$page = substr($page, 0, 255);
// Only remove file extension at the end, not all dots
@@ -318,6 +312,13 @@ class CodePressCMS {
$filePath = $this->config['content_dir'] . '/' . $pageWithoutExt;
// Prevent path traversal using realpath validation
$realContentDir = realpath($this->config['content_dir']);
$realFilePath = realpath($filePath);
if ($realFilePath && $realContentDir && strpos($realFilePath, $realContentDir) !== 0) {
return $this->getError404();
}
// Check if directory exists FIRST (directories take precedence over files)
if (is_dir($filePath)) {
return $this->getDirectoryListing($pageWithoutExt, $filePath);
@@ -325,13 +326,6 @@ class CodePressCMS {
$actualFilePath = null;
// Check if directory exists first (directories take precedence over files)
if (is_dir($filePath)) {
$directoryResult = $this->getDirectoryListing($pageWithoutExt, $filePath);
return $directoryResult;
}
// Check for exact file matches if no directory found
if (file_exists($filePath . '.md')) {
$actualFilePath = $filePath . '.md';
@@ -509,10 +503,7 @@ class CodePressCMS {
$title = trim($matches[1]);
}
// Include autoloader
require_once __DIR__ . '/../../../vendor/autoload.php';
// Configure CommonMark environment
// Configure CommonMark environment (autoloader already loaded in bootstrap)
$config = [
'html_input' => 'strip',
'allow_unsafe_links' => false,
@@ -584,7 +575,7 @@ class CodePressCMS {
return $text; // Don't link existing links, current page title, or H1 headings
}
return '<a href="?page=' . $pagePath . '&lang=' . $this->currentLanguage . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
return '<a href="?page=' . $pagePath . '&lang=' . $this->currentLanguage . '" class="auto-link" title="' . $this->t('go_to') . ' ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
};
$content = preg_replace_callback($pattern, $replacement, $content);
@@ -604,11 +595,6 @@ class CodePressCMS {
return $pages;
}
/**
* Get all page names from content directory (for navigation)
*
* @return array Associative array of page paths to display names
*/
/**
* Recursively scan for page titles in directory
*
@@ -647,37 +633,6 @@ class CodePressCMS {
}
}
/**
* Recursively scan for page names in directory (for navigation)
*
* @param string $dir Directory to scan
* @param string $prefix Relative path prefix
* @param array &$pages Reference to pages array to populate
* @return void
*/
private function scanForPageNames($dir, $prefix, &$pages) {
if (!is_dir($dir)) return;
$items = scandir($dir);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->scanForPageNames($path, $relativePath, $pages);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
// Use filename without extension as display name
$displayName = preg_replace('/\.[^.]+$/', '', $item);
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
$pages[$pagePath] = $this->formatDisplayName($displayName);
}
}
}
/**
* Format display name from filename
*
@@ -685,39 +640,33 @@ class CodePressCMS {
* @return string Formatted display name
*/
private function formatDisplayName($filename) {
// Remove language prefixes (nl. or en.) from display names
if (preg_match('/^(nl|en)\.(.+)$/', $filename, $matches)) {
// Remove language prefixes dynamically based on available languages
$availableLangs = array_keys($this->getAvailableLanguages());
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
if (preg_match($langPattern, $filename, $matches)) {
$filename = $matches[2];
}
// Remove language prefixes from directory names (nl.php-testen -> php-testen)
if (preg_match('/^(nl|en)\.php-(.+)$/', $filename, $matches)) {
$filename = 'php-' . $matches[2];
}
// Remove file extensions (.md, .php, .html) from display names
$filename = preg_replace('/\.(md|php|html)$/', '', $filename);
// Handle special cases first (only for exact filenames, not directories)
// These should only apply to actual files, not directory names
if (strtolower($filename) === 'phpinfo' && !preg_match('/\//', $filename)) {
return 'phpinfo';
}
if (strtolower($filename) === 'ict' && !preg_match('/\//', $filename)) {
return 'ICT';
// Handle special cases (case-sensitive display names)
$specialCases = [
'phpinfo' => 'phpinfo',
'ict' => 'ICT',
];
if (isset($specialCases[strtolower($filename)])) {
return $specialCases[strtolower($filename)];
}
// Replace hyphens and underscores with spaces
// Replace hyphens and underscores with spaces, then title case
$name = str_replace(['-', '_'], ' ', $filename);
// Convert to title case (first letter uppercase, rest lowercase)
$name = ucwords(strtolower($name));
// Handle other special cases
$name = str_replace('Phpinfo', 'phpinfo', $name);
$name = str_replace('Ict', 'ICT', $name);
// Post-process special cases in compound names
foreach ($specialCases as $lower => $correct) {
$name = str_ireplace(ucfirst($lower), $correct, $name);
}
return $name;
}
@@ -866,10 +815,7 @@ private function getGuidePage() {
$metadata = $parsed['metadata'];
$contentWithoutMeta = $parsed['content'];
// Include autoloader
require_once __DIR__ . '/../../../vendor/autoload.php';
// Configure CommonMark environment
// Configure CommonMark environment (autoloader already loaded in bootstrap)
$config = [
'html_input' => 'strip',
'allow_unsafe_links' => false,
@@ -993,8 +939,6 @@ private function getGuidePage() {
$hasContent = true;
}
$content .= '</div>';
if (!$hasContent) {
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
}
@@ -1167,8 +1111,11 @@ private function getGuidePage() {
* @return string Breadcrumb HTML
*/
public function generateBreadcrumb() {
// Sidebar toggle button (shown before home icon in breadcrumb)
$sidebarToggle = '<li class="breadcrumb-item sidebar-toggle-item"><button type="button" class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Toggle Sidebar" aria-label="Toggle Sidebar" aria-expanded="true"><i class="bi bi-layout-sidebar-inset"></i></button></li>';
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb">' . $sidebarToggle . '<li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
@@ -1176,12 +1123,13 @@ private function getGuidePage() {
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>';
return '<nav aria-label="breadcrumb"><ol class="breadcrumb">' . $sidebarToggle . '<li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>';
}
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">';
// Start with home icon linking to default page (root)
// Start with sidebar toggle, then home icon linking to default page (root)
$breadcrumb .= $sidebarToggle;
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li>';
// Split page path and build breadcrumb items
@@ -1190,14 +1138,15 @@ private function getGuidePage() {
foreach ($parts as $i => $part) {
$currentPath .= ($currentPath ? '/' : '') . $part;
$title = ucfirst($part);
$title = htmlspecialchars(ucfirst($part), ENT_QUOTES, 'UTF-8');
$safePath = htmlspecialchars($currentPath, ENT_QUOTES, 'UTF-8');
if ($i === count($parts) - 1) {
// Last part - active page
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $title . '</li>';
} else {
// Parent directory - clickable link with separator
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="?page=' . $currentPath . '&lang=' . $this->currentLanguage . '">' . $title . '</a></li>';
$breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="?page=' . $safePath . '&lang=' . $this->currentLanguage . '">' . $title . '</a></li>';
}
}
@@ -1287,7 +1236,6 @@ private function getGuidePage() {
private function getContentType($page) {
// Try to determine content type from page request
$pagePath = $_GET['page'] ?? $this->config['default_page'];
$pagePath = htmlspecialchars($pagePath, ENT_QUOTES, 'UTF-8');
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
$filePath = $this->config['content_dir'] . '/' . $pagePath;