- handleUpdate(): set versionError flag and return 0.0.0 if version.php missing/invalid - Dashboard stats: use 0.0.0 fallback instead of '-' when version cannot be determined - Makes version.php truly required as intended
2112 lines
78 KiB
PHP
2112 lines
78 KiB
PHP
<?php
|
|
|
|
/**
|
|
* CodePress Admin Console - Entry Point
|
|
* Access via: //admin/login|dashboard|content|config|plugins|users|logout
|
|
*/
|
|
|
|
// Security headers
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
header_remove('X-Powered-By');
|
|
|
|
// Load Composer autoloader for CommonMark
|
|
$autoloader = __DIR__ . '/../vendor/autoload.php';
|
|
if (file_exists($autoloader)) {
|
|
require_once $autoloader;
|
|
}
|
|
|
|
// Load admin components
|
|
$appConfig = require __DIR__ . '/../admin/config/app.php';
|
|
require_once __DIR__ . '/../admin/src/AdminAuth.php';
|
|
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
|
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
|
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
|
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
|
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
|
require_once __DIR__ . '/../cms/core/class/LogManager.php';
|
|
|
|
// Initialize dynamic logging from site config
|
|
$siteConfigForLogging = file_exists($appConfig['config_json'])
|
|
? (json_decode(file_get_contents($appConfig['config_json']), true) ?? [])
|
|
: [];
|
|
LogManager::init($siteConfigForLogging['logging'] ?? []);
|
|
|
|
$auth = new AdminAuth($appConfig);
|
|
|
|
// Routing
|
|
$route = $_GET['route'] ?? '';
|
|
|
|
// Public routes (no auth required)
|
|
if ($route === 'login') {
|
|
handleLogin($auth);
|
|
exit;
|
|
}
|
|
|
|
// All other routes require authentication
|
|
if (!$auth->isAuthenticated()) {
|
|
header('Location: /admin/login');
|
|
exit;
|
|
}
|
|
|
|
// Authenticated routes
|
|
switch ($route) {
|
|
case 'logout':
|
|
$auth->logout();
|
|
header('Location: /admin/login');
|
|
exit;
|
|
|
|
case 'dashboard':
|
|
case '':
|
|
handleDashboard($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content':
|
|
handleContent($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-edit':
|
|
handleContentEdit($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-new':
|
|
handleContentNew($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-delete':
|
|
handleContentDelete($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-dir-create':
|
|
handleContentDirCreate($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-dir-rename':
|
|
handleContentDirRename($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-move':
|
|
handleContentMove($auth, $appConfig);
|
|
break;
|
|
|
|
case 'content-dir-delete':
|
|
handleContentDirDelete($auth, $appConfig);
|
|
break;
|
|
|
|
case 'config':
|
|
handleConfig($auth, $appConfig);
|
|
break;
|
|
|
|
case 'security':
|
|
handleSecurity($auth, $appConfig);
|
|
break;
|
|
|
|
case 'statistics':
|
|
handleStatistics($auth, $appConfig);
|
|
break;
|
|
|
|
case 'update':
|
|
handleUpdate($auth, $appConfig);
|
|
break;
|
|
|
|
case 'theme':
|
|
handleTheme($auth, $appConfig);
|
|
break;
|
|
|
|
case 'theme-new':
|
|
handleThemeNew($auth, $appConfig);
|
|
break;
|
|
|
|
case 'theme-delete':
|
|
handleThemeDelete($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins':
|
|
handlePlugins($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-config':
|
|
handlePluginConfig($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-edit':
|
|
handlePluginEdit($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-new':
|
|
handlePluginNew($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-toggle':
|
|
handlePluginToggle($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-toggle-visibility':
|
|
handlePluginToggleVisibility($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins-delete':
|
|
handlePluginDelete($auth, $appConfig);
|
|
break;
|
|
|
|
case 'media':
|
|
handleMedia($auth, $appConfig);
|
|
break;
|
|
|
|
case 'media-list':
|
|
handleMediaList($auth, $appConfig);
|
|
break;
|
|
|
|
case 'guide':
|
|
handleGuide($auth, $appConfig);
|
|
break;
|
|
|
|
case 'logs':
|
|
handleLogs($auth, $appConfig);
|
|
break;
|
|
|
|
case 'users':
|
|
handleUsers($auth, $appConfig);
|
|
break;
|
|
|
|
default:
|
|
header('Location: /admin/dashboard');
|
|
exit;
|
|
}
|
|
|
|
// --- Route Handlers ---
|
|
|
|
function handleLogin(AdminAuth $auth): void
|
|
{
|
|
$error = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$csrfToken = $_POST['csrf_token'] ?? '';
|
|
if (!$auth->verifyCsrf($csrfToken)) {
|
|
$error = 'Ongeldige CSRF token. Probeer opnieuw.';
|
|
} else {
|
|
$username = trim($_POST['username'] ?? '');
|
|
$password = $_POST['password'] ?? '';
|
|
|
|
$result = $auth->login($username, $password);
|
|
if ($result['success']) {
|
|
$auth->regenerateCsrfToken();
|
|
header('Location: /admin/dashboard');
|
|
exit;
|
|
}
|
|
$error = $result['message'];
|
|
}
|
|
}
|
|
|
|
$csrfToken = $auth->getCsrfToken();
|
|
require __DIR__ . '/../admin/templates/login.php';
|
|
}
|
|
|
|
function handleDashboard(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
|
|
// Gather stats
|
|
$contentDir = $config['content_dir'];
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$configJson = $config['config_json'];
|
|
|
|
$versionFile = __DIR__ . '/../version.php';
|
|
$versionInfo = [];
|
|
if (file_exists($versionFile)) {
|
|
$verData = include $versionFile;
|
|
$versionInfo = is_array($verData) ? $verData : [];
|
|
}
|
|
if (empty($versionInfo['version'])) {
|
|
$versionInfo['version'] = '0.0.0';
|
|
}
|
|
|
|
$stats = [
|
|
'pages' => countFiles($contentDir, ['md', 'php', 'html']),
|
|
'directories' => countDirs($contentDir),
|
|
'plugins' => countEnabledPlugins($pluginsDir, $configJson),
|
|
'config_exists' => file_exists($configJson),
|
|
'content_size' => formatSize(dirSize($contentDir)),
|
|
'php_version' => PHP_VERSION,
|
|
'cms_version' => $versionInfo['version'],
|
|
'os' => PHP_OS_FAMILY . ' (' . php_uname('r') . ')',
|
|
];
|
|
|
|
// Load site config
|
|
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
|
|
// Load recent activity log
|
|
$logFile = $config['log_file'];
|
|
$recentLogs = [];
|
|
if (file_exists($logFile)) {
|
|
$lines = file($logFile);
|
|
$lines = array_slice($lines, -20);
|
|
foreach ($lines as $line) {
|
|
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) {
|
|
$recentLogs[] = [
|
|
'time' => $m[1],
|
|
'level' => strtolower($m[2]),
|
|
'ip' => $m[3],
|
|
'message' => $m[4],
|
|
];
|
|
}
|
|
}
|
|
$recentLogs = array_reverse($recentLogs);
|
|
}
|
|
|
|
// Load recent request log
|
|
$requestLogFile = $config['request_log'];
|
|
$requestLogger = new RequestLogger($requestLogFile);
|
|
$recentRequests = $requestLogger->getLogs(20);
|
|
|
|
// Analytics summary (last 30 days)
|
|
$siteAnalytics = is_array($siteConfig['analytics'] ?? null) ? $siteConfig['analytics'] : [];
|
|
$analytics = new Analytics($siteAnalytics);
|
|
$analyticsSummary = $analytics->getStats(30);
|
|
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleContent(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$contentDir = $config['content_dir'];
|
|
$subdir = $_GET['dir'] ?? '';
|
|
|
|
// Prevent path traversal
|
|
$subdir = str_replace(['../', '..\\'], '', $subdir);
|
|
$subdir = trim($subdir, '/');
|
|
if ($subdir === '.' || $subdir === '') {
|
|
$subdir = '';
|
|
}
|
|
$fullPath = rtrim($contentDir, '/') . '/' . $subdir;
|
|
|
|
if (!is_dir($fullPath)) {
|
|
$fullPath = $contentDir;
|
|
$subdir = '';
|
|
}
|
|
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
// Handle file upload
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file']['name'][0])) {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'zip', 'mp4', 'webm', 'ogg', 'mp3', 'wav', 'doc', 'docx', 'xls', 'xlsx'];
|
|
$uploaded = 0;
|
|
$errors = [];
|
|
foreach ($_FILES['file']['name'] as $i => $name) {
|
|
if ($_FILES['file']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowedExt)) {
|
|
$errors[] = htmlspecialchars($name) . ' (niet toegestaan type)';
|
|
continue;
|
|
}
|
|
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name);
|
|
$dest = rtrim($fullPath, '/') . '/' . $filename;
|
|
$n = 1;
|
|
while (file_exists($dest)) {
|
|
$p = pathinfo($filename);
|
|
$dest = rtrim($fullPath, '/') . '/' . $p['filename'] . '_' . $n . '.' . ($p['extension'] ?? $ext);
|
|
$n++;
|
|
}
|
|
if (move_uploaded_file($_FILES['file']['tmp_name'][$i], $dest)) {
|
|
$uploaded++;
|
|
} else {
|
|
$errors[] = htmlspecialchars($name);
|
|
}
|
|
}
|
|
if ($uploaded > 0) {
|
|
$message = $uploaded . ' bestand(en) geüpload.';
|
|
$messageType = 'success';
|
|
}
|
|
if (!empty($errors)) {
|
|
$message .= ' Fouten: ' . implode(', ', $errors);
|
|
$messageType = $messageType ?: 'danger';
|
|
}
|
|
}
|
|
}
|
|
|
|
$items = scanContentDir($fullPath, $subdir);
|
|
$route = 'content';
|
|
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function adminLog(array $config, string $level, string $message): void
|
|
{
|
|
$logFile = $config['log_file'];
|
|
$dir = dirname($logFile);
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0755, true);
|
|
}
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$ip = RequestLogger::getClientIp();
|
|
@file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
|
|
|
// Also record through the dynamic log manager (admin event)
|
|
if (class_exists('LogManager')) {
|
|
LogManager::log(LogManager::EVENT_ADMIN, $level, $message, ['ip' => $ip]);
|
|
}
|
|
}
|
|
|
|
function handleContentEdit(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$contentDir = $config['content_dir'];
|
|
$file = $_GET['file'] ?? '';
|
|
$file = str_replace(['../', '..\\'], '', $file);
|
|
$filePath = rtrim($contentDir, '/') . '/' . $file;
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
// Validate path
|
|
$realPath = realpath($filePath);
|
|
$realContentDir = realpath($contentDir);
|
|
if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) {
|
|
header('Location: /admin/content');
|
|
exit;
|
|
}
|
|
|
|
$fileExt = pathinfo($filePath, PATHINFO_EXTENSION);
|
|
$isEditable = in_array($fileExt, ['md', 'php', 'html']);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$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'] ?? '';
|
|
$layout = $_POST['layout'] ?? '';
|
|
if ($layout && in_array($layout, ['sidebar-content', 'content', 'sidebar', 'content-sidebar', 'content-sidebar-reverse'])) {
|
|
$content = updateContentFrontmatter($content, 'layout', $layout);
|
|
}
|
|
$plugins = isset($_POST['plugins']) && is_array($_POST['plugins'])
|
|
? implode(', ', array_map('trim', $_POST['plugins']))
|
|
: '';
|
|
$content = updateContentFrontmatter($content, 'plugins', $plugins);
|
|
backupContentFile($filePath);
|
|
file_put_contents($filePath, $content);
|
|
if (!$wasRenamed) {
|
|
adminLog($config, 'info', $user['username'] . ' sloeg ' . basename($filePath) . ' op');
|
|
}
|
|
}
|
|
|
|
$message = 'Bestand opgeslagen.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
if ($isEditable) {
|
|
$fileContent = file_get_contents($filePath);
|
|
$currentLayout = parseFrontmatterField($fileContent, 'layout', 'sidebar-content');
|
|
} else {
|
|
$fileContent = '';
|
|
$currentLayout = 'sidebar-content';
|
|
}
|
|
|
|
// Load available layouts from the active theme's theme.json
|
|
$themeLayouts = [];
|
|
$configJson = $config['config_json'];
|
|
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
if (!is_array($configData)) $configData = [];
|
|
$activeThemeName = $configData['active_theme'] ?? 'default';
|
|
$activeThemeFile = $config['codepress_root'] . '/themes/' . $activeThemeName . '/theme.json';
|
|
if (file_exists($activeThemeFile)) {
|
|
$themeJson = json_decode(file_get_contents($activeThemeFile), true) ?? [];
|
|
$themeLayouts = $themeJson['template'] ?? [];
|
|
}
|
|
|
|
$fileName = basename($filePath);
|
|
$route = 'content-edit';
|
|
|
|
// Get available plugins for per-page visibility
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$availablePlugins = [];
|
|
if (is_dir($pluginsDir)) {
|
|
$items = scandir($pluginsDir);
|
|
sort($items);
|
|
foreach ($items as $item) {
|
|
if ($item[0] === '.') continue;
|
|
$pluginPath = $pluginsDir . '/' . $item;
|
|
if (!is_dir($pluginPath) || !file_exists($pluginPath . '/' . $item . '.php')) continue;
|
|
$hasConfig = file_exists($pluginPath . '/config.json');
|
|
$pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : [];
|
|
if (!($pluginConfig['viewable'] ?? true)) continue;
|
|
$availablePlugins[] = $item;
|
|
}
|
|
}
|
|
$currentPlugins = parseFrontmatterField($fileContent ?? '', 'plugins', '');
|
|
$selectedPlugins = !empty($currentPlugins)
|
|
? array_map('trim', explode(',', $currentPlugins))
|
|
: [];
|
|
|
|
// Get current language for preview links
|
|
$siteConfig = file_exists($config['config_json']) ? json_decode(file_get_contents($config['config_json']), true) : [];
|
|
$currentLang = $siteConfig['language']['default'] ?? 'nl';
|
|
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleContentNew(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$contentDir = $config['content_dir'];
|
|
$dir = $_GET['dir'] ?? '';
|
|
$dir = str_replace(['../', '..\\'], '', $dir);
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$filename = trim($_POST['filename'] ?? '');
|
|
$content = $_POST['content'] ?? '';
|
|
$type = $_POST['type'] ?? 'md';
|
|
|
|
if (empty($filename)) {
|
|
$message = 'Bestandsnaam is verplicht.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
// Sanitize filename
|
|
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $filename);
|
|
if (!preg_match('/\.(md|php|html)$/', $filename)) {
|
|
$filename .= '.' . $type;
|
|
}
|
|
$targetDir = rtrim($contentDir, '/') . '/' . $dir;
|
|
$filePath = $targetDir . '/' . $filename;
|
|
|
|
if (file_exists($filePath)) {
|
|
$message = 'Bestand bestaat al.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
if (!is_dir($targetDir)) {
|
|
mkdir($targetDir, 0755, true);
|
|
}
|
|
|
|
// Build frontmatter
|
|
$frontmatterLines = [];
|
|
$layout = $_POST['layout'] ?? '';
|
|
if ($layout && $layout !== 'sidebar-content' && in_array($layout, ['sidebar-content', 'content', 'sidebar', 'content-sidebar', 'content-sidebar-reverse'])) {
|
|
$frontmatterLines[] = 'layout: ' . $layout;
|
|
}
|
|
$plugins = isset($_POST['plugins']) && is_array($_POST['plugins'])
|
|
? implode(', ', array_map('trim', $_POST['plugins']))
|
|
: '';
|
|
if (!empty($plugins)) {
|
|
$frontmatterLines[] = 'plugins: ' . $plugins;
|
|
}
|
|
if (!empty($frontmatterLines)) {
|
|
$content = "---\n" . implode("\n", $frontmatterLines) . "\n---\n" . $content;
|
|
}
|
|
|
|
file_put_contents($filePath, $content);
|
|
adminLog($config, 'info', $user['username'] . ' maakte ' . $filename . ' aan in ' . $dir);
|
|
header('Location: /admin/content?dir=' . urlencode($dir));
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get available plugins for per-page visibility
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$availablePlugins = [];
|
|
if (is_dir($pluginsDir)) {
|
|
$items = scandir($pluginsDir);
|
|
sort($items);
|
|
foreach ($items as $item) {
|
|
if ($item[0] === '.') continue;
|
|
$pluginPath = $pluginsDir . '/' . $item;
|
|
if (!is_dir($pluginPath) || !file_exists($pluginPath . '/' . $item . '.php')) continue;
|
|
$hasConfig = file_exists($pluginPath . '/config.json');
|
|
$pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : [];
|
|
if (!($pluginConfig['viewable'] ?? true)) continue;
|
|
$availablePlugins[] = $item;
|
|
}
|
|
}
|
|
|
|
// Load available layouts from the active theme's .twig files
|
|
$themeLayouts = [];
|
|
$configJson = $config['config_json'];
|
|
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
if (!is_array($configData)) $configData = [];
|
|
$activeThemeName = $configData['active_theme'] ?? 'default';
|
|
$activeThemeFile = $config['codepress_root'] . '/themes/' . $activeThemeName . '/theme.json';
|
|
if (file_exists($activeThemeFile)) {
|
|
$themeJson = json_decode(file_get_contents($activeThemeFile), true) ?? [];
|
|
$themeLayouts = $themeJson['template'] ?? [];
|
|
}
|
|
|
|
$route = 'content-new';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleContentDelete(AdminAuth $auth, array $config): void
|
|
{
|
|
$contentDir = $config['content_dir'];
|
|
$file = $_GET['file'] ?? '';
|
|
$file = str_replace(['../', '..\\'], '', $file);
|
|
$filePath = rtrim($contentDir, '/') . '/' . $file;
|
|
|
|
$realPath = realpath($filePath);
|
|
$realContentDir = realpath($contentDir);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST'
|
|
&& $auth->verifyCsrf($_POST['csrf_token'] ?? '')
|
|
&& $realPath && $realContentDir
|
|
&& strpos($realPath, $realContentDir) === 0
|
|
) {
|
|
if (is_file($filePath)) {
|
|
$user = $auth->getCurrentUser();
|
|
adminLog($config, 'info', $user['username'] . ' verwijderde ' . basename($filePath));
|
|
unlink($filePath);
|
|
}
|
|
}
|
|
|
|
$dir = dirname($file);
|
|
header('Location: /admin/content?dir=' . urlencode($dir === '.' ? '' : $dir));
|
|
exit;
|
|
}
|
|
|
|
function handleContentDirCreate(AdminAuth $auth, array $config): void
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
header('Location: /admin/content');
|
|
exit;
|
|
}
|
|
|
|
$contentDir = $config['content_dir'];
|
|
$subdir = $_GET['dir'] ?? '';
|
|
$subdir = str_replace(['../', '..\\'], '', $subdir);
|
|
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
header('Location: /admin/content?dir=' . urlencode($subdir));
|
|
exit;
|
|
}
|
|
|
|
$dirname = trim($_POST['dirname'] ?? '');
|
|
if (!empty($dirname)) {
|
|
$dirname = preg_replace('/[^a-zA-Z0-9._-]/', '-', $dirname);
|
|
$targetDir = rtrim($contentDir, '/') . '/' . ($subdir ? $subdir . '/' . $dirname : $dirname);
|
|
if (!file_exists($targetDir)) {
|
|
mkdir($targetDir, 0755, true);
|
|
}
|
|
}
|
|
|
|
header('Location: /admin/content?dir=' . urlencode($subdir));
|
|
exit;
|
|
}
|
|
|
|
function handleContentDirRename(AdminAuth $auth, array $config): void
|
|
{
|
|
$contentDir = $config['content_dir'];
|
|
$dir = $_GET['dir'] ?? '';
|
|
$dir = str_replace(['../', '..\\'], '', $dir);
|
|
$fullPath = rtrim($contentDir, '/') . '/' . $dir;
|
|
|
|
$realPath = realpath($fullPath);
|
|
$realContentDir = realpath($contentDir);
|
|
|
|
if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0 || !is_dir($fullPath)) {
|
|
header('Location: /admin/content');
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$parentDirPath = dirname($fullPath);
|
|
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$newName = trim($_POST['new_name'] ?? '');
|
|
if (!empty($newName)) {
|
|
$newName = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newName);
|
|
$newPath = $parentDirPath . '/' . $newName;
|
|
|
|
if (!file_exists($newPath)) {
|
|
rename($fullPath, $newPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
$parentRelative = dirname($dir);
|
|
header('Location: /admin/content?dir=' . urlencode($parentRelative === '.' ? '' : $parentRelative));
|
|
exit;
|
|
}
|
|
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$route = 'content-dir-rename';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleContentMove(AdminAuth $auth, array $config): void
|
|
{
|
|
$contentDir = $config['content_dir'];
|
|
$item = $_GET['item'] ?? '';
|
|
$item = str_replace(['../', '..\\'], '', $item);
|
|
$item = ltrim($item, './');
|
|
$fullPath = rtrim($contentDir, '/') . '/' . $item;
|
|
|
|
$realPath = realpath($fullPath);
|
|
$realContentDir = realpath($contentDir);
|
|
|
|
if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) {
|
|
header('Location: /admin/content');
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$targetDir = trim($_POST['target_dir'] ?? '');
|
|
$targetDir = str_replace(['../', '..\\'], '', $targetDir);
|
|
$targetFullPath = rtrim($contentDir, '/') . '/' . $targetDir;
|
|
$realTarget = realpath($targetFullPath);
|
|
|
|
if ($realTarget && strpos($realTarget, $realContentDir) === 0 && is_dir($realTarget)) {
|
|
$destPath = $realTarget . '/' . basename($fullPath);
|
|
if (!file_exists($destPath)) {
|
|
rename($fullPath, $destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
$parentRelative = is_file($fullPath) ? dirname($item) : dirname($item);
|
|
header('Location: /admin/content?dir=' . urlencode($parentRelative === '.' ? '' : $parentRelative));
|
|
exit;
|
|
}
|
|
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
|
|
// Get all available content subdirectories for the move dropdown
|
|
$dirs = getAllContentDirs($contentDir, $realContentDir);
|
|
|
|
$route = 'content-move';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleContentDirDelete(AdminAuth $auth, array $config): void
|
|
{
|
|
$contentDir = $config['content_dir'];
|
|
$dir = $_GET['dir'] ?? '';
|
|
$dir = str_replace(['../', '..\\'], '', $dir);
|
|
$fullPath = rtrim($contentDir, '/') . '/' . $dir;
|
|
|
|
$realPath = realpath($fullPath);
|
|
$realContentDir = realpath($contentDir);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST'
|
|
&& $auth->verifyCsrf($_POST['csrf_token'] ?? '')
|
|
&& $realPath && $realContentDir
|
|
&& strpos($realPath, $realContentDir) === 0
|
|
&& is_dir($fullPath)
|
|
) {
|
|
// Only delete empty directories
|
|
$items = array_diff(scandir($fullPath), ['.', '..']);
|
|
if (empty($items)) {
|
|
rmdir($fullPath);
|
|
}
|
|
}
|
|
|
|
$parentDir = dirname($dir);
|
|
header('Location: /admin/content?dir=' . urlencode($parentDir === '.' ? '' : $parentDir));
|
|
exit;
|
|
}
|
|
|
|
function handleConfig(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$configJson = $config['config_json'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
// Load current config
|
|
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
if (!is_array($configData)) $configData = [];
|
|
|
|
// Get available pages (including subdirectories)
|
|
$contentDir = $config['content_dir'];
|
|
$availablePages = [];
|
|
$availableLangs = $configData['language']['available'] ?? ['nl', 'en'];
|
|
if (is_dir($contentDir)) {
|
|
$pageMap = [];
|
|
$realContentDir = realpath($contentDir);
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iterator as $fileInfo) {
|
|
if (!$fileInfo->isFile()) continue;
|
|
$ext = $fileInfo->getExtension();
|
|
if (!in_array($ext, ['md', 'php', 'html'])) continue;
|
|
$relative = substr($fileInfo->getRealPath(), strlen($realContentDir) + 1);
|
|
$base = $relative;
|
|
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
|
|
if (preg_match($langPattern, $base, $m)) {
|
|
$base = $m[2];
|
|
}
|
|
$pageKey = preg_replace('/\.(md|php|html)$/', '', $base);
|
|
$pageMap[$pageKey] = true;
|
|
}
|
|
$availablePages = array_keys($pageMap);
|
|
sort($availablePages);
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$configData['site_title'] = trim($_POST['site_title'] ?? 'CodePress');
|
|
$configData['default_page'] = trim($_POST['default_page'] ?? 'auto');
|
|
$configData['language']['default'] = trim($_POST['language_default'] ?? 'nl');
|
|
$configData['language']['available'] = $_POST['language_available'] ?? ['nl', 'en'];
|
|
$configData['seo']['description'] = trim($_POST['seo_description'] ?? '');
|
|
$configData['seo']['keywords'] = trim($_POST['seo_keywords'] ?? '');
|
|
$configData['author']['name'] = trim($_POST['author_name'] ?? '');
|
|
$configData['author']['website'] = stripUrlScheme(trim($_POST['author_website'] ?? ''));
|
|
$configData['show_version'] = !empty($_POST['show_version']);
|
|
$configData['features']['auto_link_pages'] = !empty($_POST['feature_auto_link']);
|
|
$configData['features']['search_enabled'] = !empty($_POST['feature_search']);
|
|
$configData['features']['breadcrumbs_enabled'] = !empty($_POST['feature_breadcrumbs']);
|
|
|
|
$parseLines = function($text) {
|
|
$lines = explode("\n", str_replace("\r", "", $text));
|
|
$clean = [];
|
|
foreach ($lines as $line) {
|
|
$item = trim($line);
|
|
if ($item !== '') {
|
|
$clean[] = $item;
|
|
}
|
|
}
|
|
return array_values(array_unique($clean));
|
|
};
|
|
$configData['analytics']['excluded_ips'] = $parseLines($_POST['excluded_ips'] ?? '');
|
|
|
|
// Logging settings
|
|
$configData['logging']['enabled'] = !empty($_POST['logging_enabled']);
|
|
$configData['logging']['driver'] = ($_POST['logging_driver'] ?? 'sqlite') === 'syslog' ? 'syslog' : 'sqlite';
|
|
$configData['logging']['syslog_host'] = trim($_POST['syslog_host'] ?? '');
|
|
$configData['logging']['syslog_port'] = max(1, min(65535, (int)($_POST['syslog_port'] ?? 514)));
|
|
$configData['logging']['syslog_facility'] = preg_replace('/[^a-z0-9]/', '', $_POST['syslog_facility'] ?? 'local0');
|
|
$configData['logging']['syslog_ident'] = trim($_POST['syslog_ident'] ?? 'codepress');
|
|
$selectedEvents = $_POST['logging_events'] ?? [];
|
|
$allEvents = ['admin', 'requests', 'errors', 'security', 'content', 'system'];
|
|
$configData['logging']['events'] = [];
|
|
foreach ($allEvents as $ev) {
|
|
$configData['logging']['events'][$ev] = in_array($ev, $selectedEvents, true);
|
|
}
|
|
|
|
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
adminLog($config, 'info', $user['username'] . ' wijzigde site configuratie');
|
|
$message = 'Configuratie opgeslagen.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
$route = 'config';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleSecurity(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$configJson = $config['config_json'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
// Load current config
|
|
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
if (!is_array($configData)) $configData = [];
|
|
|
|
$sec = $configData['security'] ?? [];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$configData['security']['block_ai_bots'] = !empty($_POST['block_ai_bots']);
|
|
$configData['security']['block_scrapers'] = !empty($_POST['block_scrapers']);
|
|
$configData['security']['block_search_engines'] = !empty($_POST['block_search_engines']);
|
|
$configData['security']['block_empty_user_agent'] = !empty($_POST['block_empty_user_agent']);
|
|
$configData['security']['rate_limit_enabled'] = !empty($_POST['rate_limit_enabled']);
|
|
$configData['security']['rate_limit_max'] = max(10, min(1000, (int)($_POST['rate_limit_max'] ?? 60)));
|
|
$configData['security']['rate_limit_window'] = max(10, min(3600, (int)($_POST['rate_limit_window'] ?? 60)));
|
|
|
|
$parseLines = function($text) {
|
|
$lines = explode("\n", str_replace("\r", "", $text));
|
|
$clean = [];
|
|
foreach ($lines as $line) {
|
|
$item = trim($line);
|
|
if ($item !== '') {
|
|
$clean[] = $item;
|
|
}
|
|
}
|
|
return array_values(array_unique($clean));
|
|
};
|
|
|
|
$configData['security']['custom_blocked_agents'] = $parseLines($_POST['custom_blocked_agents'] ?? '');
|
|
$configData['security']['blocked_ips'] = $parseLines($_POST['blocked_ips'] ?? '');
|
|
$configData['security']['allowed_ips'] = $parseLines($_POST['allowed_ips'] ?? '');
|
|
|
|
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
adminLog($config, 'info', $user['username'] . ' wijzigde beveiligingsinstellingen');
|
|
$message = 'Beveiligingsinstellingen opgeslagen.';
|
|
$messageType = 'success';
|
|
$sec = $configData['security'];
|
|
}
|
|
}
|
|
|
|
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
|
$robotsPreview = BotGuard::generateRobotsTxt($sec);
|
|
|
|
$route = 'security';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleStatistics(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$configJson = $config['config_json'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
|
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
|
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
|
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
|
|
|
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
if (!is_array($configData)) $configData = [];
|
|
|
|
$analyticsDefaults = [
|
|
'enabled' => true,
|
|
'anonymize_ip' => false,
|
|
'geoip_provider' => 'local',
|
|
'geoip_mmdb_path' => '',
|
|
'geoip_api_url' => '',
|
|
'geoip_api_key' => '',
|
|
'retention_days' => 400,
|
|
];
|
|
$ana = array_merge($analyticsDefaults, is_array($configData['analytics'] ?? null) ? $configData['analytics'] : []);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} elseif (($_POST['action'] ?? '') === 'update_geoip') {
|
|
require_once __DIR__ . '/../cli/geoip-update.php';
|
|
$result = updateGeoIPDatabase();
|
|
$message = $result['message'];
|
|
$messageType = $result['success'] ? 'success' : 'danger';
|
|
adminLog($config, 'info', $user['username'] . ' werkte de GeoIP database bij');
|
|
} elseif (($_POST['action'] ?? '') === 'reset_stats') {
|
|
$statsPath = $config['codepress_root'] . '/admin/storage/stats.json';
|
|
@unlink($statsPath);
|
|
adminLog($config, 'warning', $user['username'] . ' wiste alle statistieken');
|
|
$message = 'Alle statistieken zijn gewist.';
|
|
$messageType = 'success';
|
|
} else {
|
|
$configData['analytics']['enabled'] = !empty($_POST['analytics_enabled']);
|
|
$configData['analytics']['anonymize_ip'] = !empty($_POST['anonymize_ip']);
|
|
$provider = $_POST['geoip_provider'] ?? 'local';
|
|
$configData['analytics']['geoip_provider'] = in_array($provider, ['local', 'mmdb', 'api'], true) ? $provider : 'local';
|
|
$configData['analytics']['geoip_mmdb_path'] = trim($_POST['geoip_mmdb_path'] ?? '');
|
|
$configData['analytics']['geoip_api_url'] = trim($_POST['geoip_api_url'] ?? '');
|
|
$configData['analytics']['geoip_api_key'] = trim($_POST['geoip_api_key'] ?? '');
|
|
$configData['analytics']['retention_days'] = max(30, min(3650, (int)($_POST['retention_days'] ?? 400)));
|
|
|
|
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
adminLog($config, 'info', $user['username'] . ' wijzigde statistiek-instellingen');
|
|
$message = 'Statistiek-instellingen opgeslagen.';
|
|
$messageType = 'success';
|
|
$ana = array_merge($analyticsDefaults, $configData['analytics']);
|
|
}
|
|
}
|
|
|
|
// Period filter
|
|
$period = (int)($_GET['period'] ?? 30);
|
|
if (!in_array($period, [7, 30, 90, 0], true)) $period = 30;
|
|
|
|
$analytics = new Analytics($ana);
|
|
$stats = $analytics->getStats($period);
|
|
|
|
// Export (CSV / JSON)
|
|
if (isset($_GET['export'])) {
|
|
$format = $_GET['export'] === 'json' ? 'json' : 'csv';
|
|
$periodSlug = $period === 0 ? 'alles' : $period . 'dagen';
|
|
$filename = 'codepress-statistieken-' . $periodSlug . '-' . date('Y-m-d');
|
|
|
|
if ($format === 'json') {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename="' . $filename . '.json"');
|
|
echo json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
} else {
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename="' . $filename . '.csv"');
|
|
$out = fopen('php://output', 'w');
|
|
fwrite($out, "\xEF\xBB\xBF"); // BOM so Excel reads UTF-8 correctly
|
|
|
|
fputcsv($out, ['Sectie', 'Sleutel', 'Waarde']);
|
|
foreach ($stats['totals'] as $k => $v) {
|
|
fputcsv($out, ['Totalen', $k, $v]);
|
|
}
|
|
foreach ($stats['countries'] as $k => $v) {
|
|
fputcsv($out, ['Landen', $k . ' (' . GeoIP::getCountryName($k === 'UNKNOWN' ? null : $k) . ')', $v]);
|
|
}
|
|
foreach ($stats['pages'] as $k => $v) {
|
|
fputcsv($out, ["Pagina's", $k, $v]);
|
|
}
|
|
foreach ($stats['referrers'] as $k => $v) {
|
|
fputcsv($out, ['Verwijzers', $k, $v]);
|
|
}
|
|
foreach ($stats['daily_chart'] as $day) {
|
|
fputcsv($out, ['Per dag', $day['date'], $day['views'] . ' weergaven, ' . $day['uniques'] . ' uniek']);
|
|
}
|
|
fclose($out);
|
|
}
|
|
|
|
adminLog($config, 'info', $user['username'] . ' exporteerde statistieken (' . strtoupper($format) . ')');
|
|
exit;
|
|
}
|
|
|
|
// GeoIP database status
|
|
$geoDir = $config['codepress_root'] . '/admin/storage/geoip';
|
|
$geoMeta = null;
|
|
if (file_exists($geoDir . '/meta.json')) {
|
|
$geoMeta = json_decode(file_get_contents($geoDir . '/meta.json'), true);
|
|
}
|
|
|
|
// Auto-update the GeoIP database when it is missing or older than 35 days
|
|
$geoAutoUpdated = false;
|
|
if (($ana['geoip_provider'] ?? 'local') === 'local' && !empty($ana['enabled'])) {
|
|
$lastUpdate = !empty($geoMeta['updated']) ? strtotime($geoMeta['updated']) : 0;
|
|
$isStale = $lastUpdate === 0 || $lastUpdate < strtotime('-35 days');
|
|
$lockFile = $geoDir . '/.autoupdate';
|
|
// Only retry once a day if the download keeps failing
|
|
$recentlyTried = file_exists($lockFile) && filemtime($lockFile) > strtotime('-1 day');
|
|
|
|
if ($isStale && !$recentlyTried) {
|
|
if (!is_dir($geoDir)) {
|
|
@mkdir($geoDir, 0755, true);
|
|
}
|
|
@touch($lockFile);
|
|
require_once __DIR__ . '/../cli/geoip-update.php';
|
|
$autoResult = updateGeoIPDatabase();
|
|
if ($autoResult['success']) {
|
|
$geoMeta = $autoResult['meta'];
|
|
$geoAutoUpdated = true;
|
|
adminLog($config, 'info', 'GeoIP database automatisch bijgewerkt (was verouderd)');
|
|
if ($message === '') {
|
|
$message = 'GeoIP database was verouderd en is automatisch bijgewerkt.';
|
|
$messageType = 'info';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// World map SVG
|
|
$worldMapPath = $config['codepress_root'] . '/public/assets/img/world-map.svg';
|
|
$worldMapSvg = file_exists($worldMapPath) ? file_get_contents($worldMapPath) : '';
|
|
$worldMapSvg = preg_replace('/^<\?xml[^>]*\?>\s*/', '', $worldMapSvg);
|
|
|
|
$route = 'statistics';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleTheme(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$configJson = $config['config_json'];
|
|
$themesDir = $config['codepress_root'] . '/themes';
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? '');
|
|
if ($themeName !== '' && file_exists($themesDir . '/' . $themeName . '/theme.json')) {
|
|
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
|
$cfg['active_theme'] = $themeName;
|
|
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
adminLog($config, 'info', $user['username'] . ' activeerde thema ' . $themeName);
|
|
$message = 'Thema geactiveerd.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load active theme name
|
|
$configData = json_decode(file_get_contents($configJson), true) ?? [];
|
|
$activeTheme = $configData['active_theme'] ?? 'default';
|
|
|
|
// Load all themes
|
|
$themes = [];
|
|
if (is_dir($themesDir)) {
|
|
foreach (scandir($themesDir) as $item) {
|
|
if ($item[0] === '.') continue;
|
|
$themeJson = $themesDir . '/' . $item . '/theme.json';
|
|
if (is_dir($themesDir . '/' . $item) && file_exists($themeJson)) {
|
|
$themeData = json_decode(file_get_contents($themeJson), true) ?? [];
|
|
$themes[$item] = $themeData;
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'theme';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleThemeNew(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$themesDir = $config['codepress_root'] . '/themes';
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$newName = trim($_POST['theme_name'] ?? '');
|
|
|
|
if (empty($newName)) {
|
|
$message = 'Thema naam is verplicht.';
|
|
$messageType = 'danger';
|
|
} elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $newName)) {
|
|
$message = 'Ongeldige thema naam. Gebruik alleen letters, cijfers, streepjes en underscores. Begin met een letter.';
|
|
$messageType = 'danger';
|
|
} elseif (file_exists($themesDir . '/' . $newName . '/theme.json')) {
|
|
$message = 'Thema met deze naam bestaat al.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$newThemeDir = $themesDir . '/' . $newName;
|
|
mkdir($newThemeDir, 0755, true);
|
|
|
|
$defaults = [
|
|
'title' => $newName,
|
|
'config' => [
|
|
'default_template' => 'full_content',
|
|
],
|
|
'template' => [
|
|
'full_content' => 'full_content.twig',
|
|
'left_sidebar' => 'left_sidebar.twig',
|
|
'right_sidebar' => 'right_sidebar.twig',
|
|
'custom1' => 'custom1.twig',
|
|
],
|
|
];
|
|
file_put_contents($newThemeDir . '/theme.json', json_encode($defaults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
// Scaffold a full theme folder from the default theme
|
|
$defaultThemeDir = $themesDir . '/default';
|
|
if (is_dir($defaultThemeDir)) {
|
|
$copyDirs = ['partials', 'css', 'js'];
|
|
foreach ($copyDirs as $sub) {
|
|
$src = $defaultThemeDir . '/' . $sub;
|
|
if (is_dir($src)) {
|
|
$dst = $newThemeDir . '/' . $sub;
|
|
mkdir($dst, 0755, true);
|
|
foreach (scandir($src) as $f) {
|
|
if ($f[0] === '.') continue;
|
|
if (is_file($src . '/' . $f)) {
|
|
copy($src . '/' . $f, $dst . '/' . $f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach (['base.twig', 'full_content.twig', 'left_sidebar.twig', 'right_sidebar.twig', 'custom1.twig'] as $tpl) {
|
|
if (is_file($defaultThemeDir . '/' . $tpl)) {
|
|
copy($defaultThemeDir . '/' . $tpl, $newThemeDir . '/' . $tpl);
|
|
}
|
|
}
|
|
// Copy the default theme's preview image if present
|
|
if (is_file($defaultThemeDir . '/theme.png')) {
|
|
copy($defaultThemeDir . '/theme.png', $newThemeDir . '/theme.png');
|
|
}
|
|
}
|
|
|
|
adminLog($config, 'info', $user['username'] . ' maakte thema ' . $newName . ' aan');
|
|
header('Location: /admin/theme');
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'theme-new';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleThemeDelete(AdminAuth $auth, array $config): void
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
header('Location: /admin/theme');
|
|
exit;
|
|
}
|
|
|
|
$configJson = $config['config_json'];
|
|
$themesDir = $config['codepress_root'] . '/themes';
|
|
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['theme'] ?? '');
|
|
$themeDir = $themesDir . '/' . $themeName;
|
|
|
|
if ($themeName === '' || $themeName === 'default' || !is_dir($themeDir)) {
|
|
header('Location: /admin/theme');
|
|
exit;
|
|
}
|
|
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
// Recursively remove the entire theme directory
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($themeDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
foreach ($iterator as $file) {
|
|
if ($file->isDir()) {
|
|
@rmdir($file->getRealPath());
|
|
} else {
|
|
@unlink($file->getRealPath());
|
|
}
|
|
}
|
|
@rmdir($themeDir);
|
|
|
|
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
|
if (($cfg['active_theme'] ?? '') === $themeName) {
|
|
$cfg['active_theme'] = 'default';
|
|
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' verwijderde thema ' . $themeName);
|
|
}
|
|
|
|
header('Location: /admin/theme');
|
|
exit;
|
|
}
|
|
|
|
function handlePlugins(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$pluginsDir = $config['plugins_dir'];
|
|
|
|
// Load enabled_plugins from config.json
|
|
$siteConfig = [];
|
|
if (file_exists($config['config_json'])) {
|
|
$siteConfig = json_decode(file_get_contents($config['config_json']), true) ?? [];
|
|
}
|
|
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
|
|
$regenerateConfig = false;
|
|
|
|
$plugins = [];
|
|
|
|
if (is_dir($pluginsDir)) {
|
|
foreach (scandir($pluginsDir) as $item) {
|
|
if ($item[0] === '.') continue;
|
|
$pluginPath = $pluginsDir . '/' . $item;
|
|
if (!is_dir($pluginPath)) continue;
|
|
|
|
$hasConfig = file_exists($pluginPath . '/config.json');
|
|
$pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : [];
|
|
$hasMainFile = file_exists($pluginPath . '/' . $item . '.php');
|
|
$hasReadme = file_exists($pluginPath . '/README.md');
|
|
|
|
$isEnabled = in_array($item, $enabledPlugins, true);
|
|
|
|
// Auto-register plugin in enabled_plugins if it has a main file and is not in the list
|
|
if ($hasMainFile && !$isEnabled) {
|
|
$enabledPlugins[] = $item;
|
|
$regenerateConfig = true;
|
|
$isEnabled = true;
|
|
}
|
|
|
|
$plugins[] = [
|
|
'name' => $item,
|
|
'path' => $pluginPath,
|
|
'enabled' => $isEnabled,
|
|
'viewable' => $pluginConfig['viewable'] ?? true,
|
|
'config' => $pluginConfig,
|
|
'has_config' => $hasConfig,
|
|
'has_main' => $hasMainFile,
|
|
'has_readme' => $hasReadme,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Save updated enabled_plugins back to config.json
|
|
if ($regenerateConfig) {
|
|
$siteConfig['enabled_plugins'] = $enabledPlugins;
|
|
file_put_contents($config['config_json'], json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
$route = 'plugins';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handlePluginConfig(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$pluginName = $_GET['plugin'] ?? '';
|
|
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $pluginName);
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
$configFile = $pluginPath . '/config.json';
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if (empty($pluginName) || !is_dir($pluginPath)) {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$postConfig = $_POST['config'] ?? [];
|
|
$currentConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : [];
|
|
|
|
$newConfig = array_replace_recursive($currentConfig, $postConfig);
|
|
|
|
// Convert checkbox values: unchecked checkboxes are not sent
|
|
foreach (array_keys($currentConfig) as $key) {
|
|
if (is_bool($currentConfig[$key])) {
|
|
$newConfig[$key] = isset($postConfig[$key]) && $postConfig[$key] === '1';
|
|
}
|
|
}
|
|
|
|
file_put_contents($configFile, json_encode($newConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
$message = 'Configuratie opgeslagen voor plugin: ' . htmlspecialchars($pluginName);
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
$pluginConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : [];
|
|
$route = 'plugins-config';
|
|
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handlePluginEdit(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? '');
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
$pluginFile = $pluginPath . '/' . $pluginName . '.php';
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if (empty($pluginName) || !is_dir($pluginPath) || !file_exists($pluginFile)) {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$content = $_POST['content'] ?? '';
|
|
file_put_contents($pluginFile, $content);
|
|
$message = 'Plugin opgeslagen.';
|
|
$messageType = 'success';
|
|
|
|
// Clear opcache if available
|
|
if (function_exists('opcache_invalidate')) {
|
|
opcache_invalidate($pluginFile, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
$fileContent = file_get_contents($pluginFile);
|
|
$route = 'plugins-edit';
|
|
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handlePluginNew(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$pluginName = trim($_POST['plugin_name'] ?? '');
|
|
$pluginDesc = trim($_POST['plugin_desc'] ?? '');
|
|
|
|
if (empty($pluginName)) {
|
|
$message = 'Plugin naam is verplicht.';
|
|
$messageType = 'danger';
|
|
} elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $pluginName)) {
|
|
$message = 'Ongeldige plugin naam. Gebruik alleen letters, cijfers en underscores. Begin met een letter.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
|
|
if (is_dir($pluginPath)) {
|
|
$message = 'Plugin met deze naam bestaat al.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
mkdir($pluginPath, 0755, true);
|
|
|
|
$pluginFile = $pluginPath . '/' . $pluginName . '.php';
|
|
$boilerplate = "<?php\n\nclass $pluginName\n{\n";
|
|
$boilerplate .= " private ?CMSAPI \$api = null;\n private array \$config;\n\n";
|
|
$boilerplate .= " public function __construct()\n {\n \$this->config = [\n 'viewable' => true,\n ];\n }\n\n";
|
|
$boilerplate .= " public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n";
|
|
$boilerplate .= " public function getSidebarContent(): string\n {\n return '';\n }\n\n";
|
|
$boilerplate .= " public function getConfig(): array\n {\n return \$this->config;\n }\n\n";
|
|
$boilerplate .= " public function setConfig(array \$config): void\n {\n \$this->config = array_merge(\$this->config, \$config);\n }\n}";
|
|
file_put_contents($pluginFile, $boilerplate);
|
|
|
|
$configFile = $pluginPath . '/config.json';
|
|
file_put_contents($configFile, json_encode(['viewable' => true], JSON_PRETTY_PRINT));
|
|
|
|
if ($pluginDesc) {
|
|
$readme = "# $pluginName\n\n$pluginDesc\n";
|
|
file_put_contents($pluginPath . '/README.md', $readme);
|
|
}
|
|
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'plugins-new';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handlePluginToggle(AdminAuth $auth, array $config): void
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? '');
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
|
|
if (empty($pluginName) || !is_dir($pluginPath)) {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$siteConfig = [];
|
|
$configJson = $config['config_json'];
|
|
if (file_exists($configJson)) {
|
|
$siteConfig = json_decode(file_get_contents($configJson), true) ?? [];
|
|
}
|
|
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
|
|
|
|
if (in_array($pluginName, $enabledPlugins, true)) {
|
|
$enabledPlugins = array_values(array_filter($enabledPlugins, fn($p) => $p !== $pluginName));
|
|
} else {
|
|
$enabledPlugins[] = $pluginName;
|
|
}
|
|
|
|
$siteConfig['enabled_plugins'] = $enabledPlugins;
|
|
file_put_contents($configJson, json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' schakelde plugin ' . $pluginName . ' ' . (in_array($pluginName, $enabledPlugins, true) ? 'uit' : 'in'));
|
|
}
|
|
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
function handlePluginToggleVisibility(AdminAuth $auth, array $config): void
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? '');
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
|
|
if (empty($pluginName) || !is_dir($pluginPath)) {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$configFile = $pluginPath . '/config.json';
|
|
if (file_exists($configFile)) {
|
|
$pluginConfig = json_decode(file_get_contents($configFile), true);
|
|
$pluginConfig['viewable'] = !($pluginConfig['viewable'] ?? true);
|
|
file_put_contents($configFile, json_encode($pluginConfig, JSON_PRETTY_PRINT));
|
|
} else {
|
|
file_put_contents($configFile, json_encode(['viewable' => false], JSON_PRETTY_PRINT));
|
|
}
|
|
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' wijzigde zichtbaarheid van plugin ' . $pluginName);
|
|
}
|
|
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
function handlePluginDelete(AdminAuth $auth, array $config): void
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? '');
|
|
$pluginPath = $pluginsDir . '/' . $pluginName;
|
|
|
|
if (empty($pluginName) || !is_dir($pluginPath)) {
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$di = new RecursiveDirectoryIterator($pluginPath, RecursiveDirectoryIterator::SKIP_DOTS);
|
|
$files = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach ($files as $file) {
|
|
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
|
|
}
|
|
rmdir($pluginPath);
|
|
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' verwijderde plugin ' . $pluginName);
|
|
}
|
|
|
|
header('Location: /admin/plugins');
|
|
exit;
|
|
}
|
|
|
|
function handleMedia(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$assetsDir = $config['assets_dir'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
if (!empty($_FILES['file']['name'][0]) && is_array($_FILES['file']['name'])) {
|
|
if (!is_dir($assetsDir)) {
|
|
mkdir($assetsDir, 0755, true);
|
|
}
|
|
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'zip', 'mp4', 'webm', 'ogg', 'mp3', 'wav', 'doc', 'docx', 'xls', 'xlsx'];
|
|
$uploaded = 0;
|
|
$errors = [];
|
|
foreach ($_FILES['file']['name'] as $i => $name) {
|
|
if ($_FILES['file']['error'][$i] !== UPLOAD_ERR_OK) continue;
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowedExt)) {
|
|
$errors[] = htmlspecialchars($name) . ' (niet toegestaan type)';
|
|
continue;
|
|
}
|
|
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name);
|
|
$dest = $assetsDir . '/' . $filename;
|
|
$n = 1;
|
|
while (file_exists($dest)) {
|
|
$p = pathinfo($filename);
|
|
$dest = $assetsDir . '/' . $p['filename'] . '_' . $n . '.' . ($p['extension'] ?? $ext);
|
|
$n++;
|
|
}
|
|
if (move_uploaded_file($_FILES['file']['tmp_name'][$i], $dest)) {
|
|
$uploaded++;
|
|
} else {
|
|
$errors[] = htmlspecialchars($name);
|
|
}
|
|
}
|
|
if ($uploaded > 0) {
|
|
$message = $uploaded . ' bestand(en) geüpload.';
|
|
$messageType = 'success';
|
|
}
|
|
if (!empty($errors)) {
|
|
$message .= ' Fouten: ' . implode(', ', $errors);
|
|
$messageType = $messageType ?: 'danger';
|
|
}
|
|
}
|
|
|
|
if (!empty($_POST['delete'])) {
|
|
$file = basename($_POST['delete']);
|
|
$path = $assetsDir . '/' . $file;
|
|
if (file_exists($path)) {
|
|
unlink($path);
|
|
$message = 'Bestand verwijderd.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// List files
|
|
$files = [];
|
|
if (is_dir($assetsDir)) {
|
|
foreach (scandir($assetsDir) as $item) {
|
|
if ($item[0] === '.' || is_dir($assetsDir . '/' . $item)) continue;
|
|
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
|
$isImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']);
|
|
$files[] = [
|
|
'name' => $item,
|
|
'url' => '/-assets/' . $item,
|
|
'size' => filesize($assetsDir . '/' . $item),
|
|
'is_image' => $isImage,
|
|
'ext' => $ext,
|
|
'modified' => date('Y-m-d H:i', filemtime($assetsDir . '/' . $item)),
|
|
];
|
|
}
|
|
}
|
|
// Sort newest first
|
|
usort($files, fn($a, $b) => strcmp($b['modified'], $a['modified']));
|
|
|
|
$route = 'media';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleMediaList(AdminAuth $auth, array $config): void
|
|
{
|
|
$contentDir = realpath(__DIR__ . '/../content');
|
|
$files = [];
|
|
|
|
if ($contentDir === false || !is_dir($contentDir)) {
|
|
header('Content-Type: application/json');
|
|
echo json_encode([]);
|
|
exit;
|
|
}
|
|
|
|
$mediaExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'mp3', 'wav', 'ogg', 'mp4', 'webm'];
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $path => $info) {
|
|
if ($info->isDir()) continue;
|
|
|
|
$rel = substr($path, strlen($contentDir) + 1);
|
|
if (strpos($rel, '.') === 0) continue;
|
|
|
|
$ext = strtolower($info->getExtension());
|
|
if (!in_array($ext, $mediaExts)) continue;
|
|
|
|
$isImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']);
|
|
$isAudio = in_array($ext, ['mp3', 'wav', 'ogg']);
|
|
$isVideo = in_array($ext, ['mp4', 'webm']);
|
|
|
|
$files[] = [
|
|
'name' => $info->getFilename(),
|
|
'url' => '/-media/' . $rel,
|
|
'ext' => $ext,
|
|
'is_image' => $isImage,
|
|
'is_audio' => $isAudio,
|
|
'is_video' => $isVideo,
|
|
];
|
|
}
|
|
|
|
usort($files, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode($files);
|
|
exit;
|
|
}
|
|
|
|
function handleUsers(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
if ($action === 'add') {
|
|
$result = $auth->addUser(
|
|
trim($_POST['username'] ?? ''),
|
|
$_POST['password'] ?? '',
|
|
$_POST['role'] ?? 'admin'
|
|
);
|
|
$message = $result['message'];
|
|
$messageType = $result['success'] ? 'success' : 'danger';
|
|
} elseif ($action === 'delete') {
|
|
$result = $auth->deleteUser($_POST['delete_username'] ?? '');
|
|
$message = $result['message'];
|
|
$messageType = $result['success'] ? 'success' : 'danger';
|
|
} elseif ($action === 'change_password') {
|
|
$result = $auth->changePassword(
|
|
$_POST['pw_username'] ?? '',
|
|
$_POST['new_password'] ?? ''
|
|
);
|
|
$message = $result['message'];
|
|
$messageType = $result['success'] ? 'success' : 'danger';
|
|
} elseif ($action === 'change_own_password') {
|
|
$currentPassword = $_POST['current_password'] ?? '';
|
|
$newPassword = $_POST['new_password'] ?? '';
|
|
$confirmPassword = $_POST['confirm_password'] ?? '';
|
|
if ($newPassword !== $confirmPassword) {
|
|
$message = 'De nieuwe wachtwoorden komen niet overeen.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$result = $auth->changeOwnPassword(
|
|
$user['username'],
|
|
$currentPassword,
|
|
$newPassword
|
|
);
|
|
$message = $result['message'];
|
|
$messageType = $result['success'] ? 'success' : 'danger';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$users = $auth->getUsers();
|
|
$route = 'users';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
/**
|
|
* Render the admin guide page from a markdown file
|
|
*
|
|
* Reads the guide file for the selected language, renders it through
|
|
* CommonMark (with HeadingPermalink IDs moved to heading elements for
|
|
* working deep links), and outputs it via the admin layout.
|
|
*
|
|
* @param AdminAuth $auth Admin authentication instance
|
|
* @param array $config Site configuration
|
|
*/
|
|
function handleGuide(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
|
|
// Determine language for guide
|
|
$lang = $_GET['lang'] ?? 'nl';
|
|
if (!in_array($lang, ['nl', 'en'])) $lang = 'nl';
|
|
|
|
$guideFile = __DIR__ . '/../guide/' . $lang . '.codepress.md';
|
|
if (!file_exists($guideFile)) {
|
|
$guideFile = __DIR__ . '/../guide/en.codepress.md';
|
|
$lang = 'en';
|
|
}
|
|
|
|
$rawContent = file_get_contents($guideFile);
|
|
|
|
// Parse markdown using CommonMark if available
|
|
$content = '';
|
|
if (class_exists('League\CommonMark\MarkdownConverter')) {
|
|
$config = [
|
|
'html_input' => 'allow',
|
|
'allow_unsafe_links' => false,
|
|
'max_nesting_level' => 100,
|
|
'heading_permalink' => [
|
|
'html_class' => 'heading-permalink',
|
|
'id_prefix' => '',
|
|
'insert' => 'after',
|
|
'min_heading_level' => 1,
|
|
'max_heading_level' => 6,
|
|
'title' => 'Permalink',
|
|
'symbol' => '',
|
|
'aria_hidden' => true,
|
|
],
|
|
];
|
|
$environment = new \League\CommonMark\Environment\Environment($config);
|
|
$environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension());
|
|
$environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension());
|
|
$environment->addExtension(new \League\CommonMark\Extension\Strikethrough\StrikethroughExtension());
|
|
$environment->addExtension(new \League\CommonMark\Extension\TaskList\TaskListExtension());
|
|
$environment->addExtension(new \League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension());
|
|
$converter = new \League\CommonMark\MarkdownConverter($environment);
|
|
$content = $converter->convert($rawContent)->getContent();
|
|
|
|
// Move IDs from heading-permalink anchors to their parent headings
|
|
// so deep links scroll to the heading element itself (not a hidden anchor)
|
|
$content = preg_replace(
|
|
'~<h(\d)>(.*?)<a id="([^"]+)"[^>]*></a></h\1>~s',
|
|
'<h\1 id="\3">\2</h\1>',
|
|
$content
|
|
);
|
|
} else {
|
|
$content = '<pre>' . htmlspecialchars($rawContent) . '</pre>';
|
|
}
|
|
|
|
$route = 'guide';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleLogs(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
|
|
// Filters
|
|
$filterEvent = $_GET['event'] ?? '';
|
|
$filterLevel = $_GET['level'] ?? '';
|
|
$filterSearch = trim($_GET['search'] ?? '');
|
|
$limit = max(10, min(1000, (int)($_GET['limit'] ?? 200)));
|
|
|
|
// Clear
|
|
if (isset($_GET['clear'])) {
|
|
LogManager::clear();
|
|
$message = 'Log gewist.';
|
|
header('Location: /admin/logs');
|
|
exit;
|
|
}
|
|
|
|
// Download
|
|
if (isset($_GET['download'])) {
|
|
$entries = LogManager::getLogs(1000, $filterEvent ?: null, $filterLevel ?: null, $filterSearch ?: null);
|
|
$out = '';
|
|
foreach ($entries as $e) {
|
|
$out .= '[' . $e['time'] . '] [' . $e['event'] . '] [' . $e['level'] . '] [' . $e['ip'] . '] ' . $e['message'] . "\n";
|
|
}
|
|
header('Content-Type: text/plain');
|
|
header('Content-Disposition: attachment; filename="codepress.log"');
|
|
echo $out;
|
|
exit;
|
|
}
|
|
|
|
// Read log entries with filters
|
|
$logEntries = LogManager::getLogs($limit, $filterEvent ?: null, $filterLevel ?: null, $filterSearch ?: null);
|
|
|
|
// Available event types for the filter dropdown
|
|
$eventTypes = ['admin', 'requests', 'errors', 'security', 'content', 'system'];
|
|
$levelTypes = ['info', 'warning', 'error', 'debug'];
|
|
|
|
$route = 'logs';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
function handleUpdate(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$message = '';
|
|
$messageType = '';
|
|
$updateOutput = '';
|
|
|
|
// Load CMS version
|
|
$versionFile = $config['codepress_root'] . '/version.php';
|
|
$cmsVersion = '0.0.0';
|
|
$versionError = false;
|
|
if (!file_exists($versionFile)) {
|
|
$versionError = true;
|
|
$cmsVersion = '0.0.0';
|
|
} else {
|
|
$verData = require $versionFile;
|
|
if (!is_array($verData) || empty($verData['version'])) {
|
|
$versionError = true;
|
|
$cmsVersion = '0.0.0';
|
|
} else {
|
|
$cmsVersion = $verData['version'];
|
|
}
|
|
}
|
|
|
|
// Get git branch & check git permissions
|
|
$gitBranch = 'main';
|
|
$root = $config['codepress_root'];
|
|
$gitDir = $root . '/.git';
|
|
$isGitWritable = is_dir($gitDir) && (is_writable($gitDir) && (!is_dir($gitDir . '/objects') || is_writable($gitDir . '/objects')));
|
|
|
|
if (function_exists('exec')) {
|
|
@exec('git rev-parse --abbrev-ref HEAD 2>&1', $branchOutput);
|
|
if (!empty($branchOutput[0]) && strpos($branchOutput[0], 'fatal') === false) {
|
|
$gitBranch = trim($branchOutput[0]);
|
|
}
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$root = $config['codepress_root'];
|
|
$cmd = 'cd ' . escapeshellarg($root) . ' && git pull origin ' . escapeshellarg($gitBranch) . ' 2>&1';
|
|
|
|
$outputLines = [];
|
|
$returnCode = -1;
|
|
if (function_exists('exec')) {
|
|
@exec($cmd, $outputLines, $returnCode);
|
|
$updateOutput = implode("\n", $outputLines);
|
|
} else {
|
|
$updateOutput = 'exec() is uitgeschakeld op de PHP server.';
|
|
$returnCode = 1;
|
|
}
|
|
|
|
if ($returnCode === 0) {
|
|
adminLog($config, 'info', $user['username'] . ' voerde een succesvolle systeemupdate uit');
|
|
$message = 'Systeem succesvol bijgewerkt naar de nieuwste versie!';
|
|
$messageType = 'success';
|
|
} else {
|
|
adminLog($config, 'warning', $user['username'] . ' probeerde een systeemupdate uit te voeren: ' . $updateOutput);
|
|
$message = 'Fout bij het bijwerken van het systeem. Bekijk de resultaten hieronder.';
|
|
$messageType = 'danger';
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'update';
|
|
require __DIR__ . '/../admin/templates/layout.php';
|
|
}
|
|
|
|
// --- Frontmatter helpers ---
|
|
|
|
/**
|
|
* Remove the URL scheme (http://, https://) from a website value so it is
|
|
* stored as a bare hostname. The engine adds the scheme back when rendering.
|
|
*
|
|
* @param string $url Raw URL value
|
|
* @return string Hostname without scheme
|
|
*/
|
|
function stripUrlScheme(string $url): string
|
|
{
|
|
$url = trim($url);
|
|
if (preg_match('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', $url)) {
|
|
$url = preg_replace('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', '', $url);
|
|
}
|
|
return rtrim($url, '/');
|
|
}
|
|
|
|
/**
|
|
* Keep a timestamped copy of a content file before it is overwritten.
|
|
*
|
|
* Backups live in a hidden -backups directory inside the content folder so the
|
|
* CMS itself skips them, and only the newest few versions per file are kept.
|
|
*/
|
|
function backupContentFile(string $filePath, int $keep = 5): void
|
|
{
|
|
if (!is_file($filePath) || filesize($filePath) === 0) {
|
|
return;
|
|
}
|
|
|
|
$contentRoot = realpath(dirname(__DIR__) . '/content');
|
|
$realFile = realpath($filePath);
|
|
|
|
if (!$contentRoot || !$realFile || strpos($realFile, $contentRoot) !== 0) {
|
|
return;
|
|
}
|
|
|
|
$backupDir = $contentRoot . '/-backups';
|
|
if (!is_dir($backupDir) && !@mkdir($backupDir, 0755, true) && !is_dir($backupDir)) {
|
|
return;
|
|
}
|
|
|
|
// Mirror the relative path so files with the same name never collide
|
|
$relative = ltrim(substr($realFile, strlen($contentRoot)), '/');
|
|
$flat = str_replace('/', '__', $relative);
|
|
|
|
@copy($realFile, $backupDir . '/' . $flat . '.' . date('Ymd-His') . '.bak');
|
|
|
|
// Prune older versions of this specific file
|
|
$existing = glob($backupDir . '/' . $flat . '.*.bak') ?: [];
|
|
if (count($existing) > $keep) {
|
|
sort($existing); // filenames sort chronologically
|
|
foreach (array_slice($existing, 0, count($existing) - $keep) as $old) {
|
|
@unlink($old);
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseFrontmatterField(string $content, string $key, string $default = ''): string
|
|
{
|
|
if (preg_match('/^---\s*\n(.*?)\n---\s*\n/s', $content, $matches)) {
|
|
$metaContent = $matches[1];
|
|
foreach (explode("\n", $metaContent) as $line) {
|
|
if (strpos($line, $key . ':') === 0) {
|
|
[, $value] = explode(':', $line, 2);
|
|
return trim($value, " \t'\"");
|
|
}
|
|
}
|
|
}
|
|
return $default;
|
|
}
|
|
|
|
function updateContentFrontmatter(string $content, string $key, string $value): string
|
|
{
|
|
if (preg_match('/^(---\s*\n)(.*?)(\n---\s*\n.*)$/s', $content, $matches)) {
|
|
$metaContent = $matches[2];
|
|
$body = $matches[3];
|
|
$found = false;
|
|
$lines = explode("\n", $metaContent);
|
|
foreach ($lines as $i => $line) {
|
|
if (strpos($line, $key . ':') === 0) {
|
|
$lines[$i] = $key . ': ' . $value;
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
$lines[] = $key . ': ' . $value;
|
|
}
|
|
return $matches[1] . implode("\n", $lines) . $body;
|
|
} else {
|
|
return "---\n" . $key . ': ' . $value . "\n---\n" . $content;
|
|
}
|
|
}
|
|
|
|
// --- Helper functions ---
|
|
|
|
function countFiles(string $dir, array $extensions): int
|
|
{
|
|
$count = 0;
|
|
if (!is_dir($dir)) return 0;
|
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile() && in_array($file->getExtension(), $extensions)) {
|
|
$count++;
|
|
}
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
function countDirs(string $dir): int
|
|
{
|
|
if (!is_dir($dir)) return 0;
|
|
$count = 0;
|
|
foreach (scandir($dir) as $item) {
|
|
if ($item[0] !== '.' && is_dir($dir . '/' . $item)) $count++;
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
function countEnabledPlugins(string $pluginsDir, string $configJson): int
|
|
{
|
|
if (!is_dir($pluginsDir)) return 0;
|
|
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
$enabled = $siteConfig['enabled_plugins'] ?? [];
|
|
return count(array_filter($enabled, fn($p) => is_dir($pluginsDir . '/' . $p)));
|
|
}
|
|
|
|
function dirSize(string $dir): int
|
|
{
|
|
$size = 0;
|
|
if (!is_dir($dir)) return 0;
|
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) $size += $file->getSize();
|
|
}
|
|
return $size;
|
|
}
|
|
|
|
function formatSize(int $bytes): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB'];
|
|
$i = 0;
|
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
|
$bytes /= 1024;
|
|
$i++;
|
|
}
|
|
return round($bytes, 1) . ' ' . $units[$i];
|
|
}
|
|
|
|
function scanContentDir(string $fullPath, string $subdir): array
|
|
{
|
|
$items = [];
|
|
if (!is_dir($fullPath)) return $items;
|
|
|
|
foreach (scandir($fullPath) as $item) {
|
|
if ($item[0] === '.') continue;
|
|
// Skip assets directory in content listing
|
|
if ($item === 'assets' && is_dir($itemPath)) continue;
|
|
$itemPath = $fullPath . '/' . $item;
|
|
$relativePath = $subdir ? $subdir . '/' . $item : $item;
|
|
|
|
$items[] = [
|
|
'name' => $item,
|
|
'path' => $relativePath,
|
|
'is_dir' => is_dir($itemPath),
|
|
'size' => is_file($itemPath) ? formatSize(filesize($itemPath)) : '',
|
|
'modified' => date('d-m-Y H:i', filemtime($itemPath)),
|
|
'extension' => is_file($itemPath) ? pathinfo($item, PATHINFO_EXTENSION) : '',
|
|
];
|
|
}
|
|
|
|
// Sort: directories first, then files alphabetically
|
|
usort($items, function ($a, $b) {
|
|
if ($a['is_dir'] !== $b['is_dir']) return $b['is_dir'] - $a['is_dir'];
|
|
return strcasecmp($a['name'], $b['name']);
|
|
});
|
|
|
|
return $items;
|
|
}
|
|
|
|
function getAllContentDirs(string $contentDir, string $realContentDir): array
|
|
{
|
|
$dirs = [];
|
|
if (!is_dir($contentDir)) return $dirs;
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $path => $fileInfo) {
|
|
if (!$fileInfo->isDir()) continue;
|
|
$realPath = $fileInfo->getRealPath();
|
|
if (strpos($realPath, $realContentDir) !== 0) continue;
|
|
$relative = substr($realPath, strlen($realContentDir) + 1);
|
|
if ($relative === false || $relative === '') continue;
|
|
// Skip hidden directories (starting with - or .)
|
|
$base = basename($relative);
|
|
if ($base[0] === '-' || $base[0] === '.') continue;
|
|
$dirs[] = $relative;
|
|
}
|
|
|
|
sort($dirs);
|
|
return $dirs;
|
|
}
|