File-based admin panel accessible at /admin.php with: - Session-based auth with bcrypt hashing and brute-force protection - Dashboard with site statistics and quick actions - Content manager: browse, create, edit, delete files - Config editor with JSON validation - Plugin overview with status indicators - User management: add, remove, change passwords - CSRF protection on all forms, path traversal prevention - Updated README (NL/EN) and guides with admin documentation
450 lines
13 KiB
PHP
450 lines
13 KiB
PHP
<?php
|
|
|
|
/**
|
|
* CodePress Admin Console - Entry Point
|
|
* Access via: /admin.php?route=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 admin components
|
|
$appConfig = require __DIR__ . '/../admin-console/config/app.php';
|
|
require_once __DIR__ . '/../admin-console/src/AdminAuth.php';
|
|
|
|
$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.php?route=login');
|
|
exit;
|
|
}
|
|
|
|
// Authenticated routes
|
|
switch ($route) {
|
|
case 'logout':
|
|
$auth->logout();
|
|
header('Location: admin.php?route=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 'config':
|
|
handleConfig($auth, $appConfig);
|
|
break;
|
|
|
|
case 'plugins':
|
|
handlePlugins($auth, $appConfig);
|
|
break;
|
|
|
|
case 'users':
|
|
handleUsers($auth, $appConfig);
|
|
break;
|
|
|
|
default:
|
|
header('Location: admin.php?route=dashboard');
|
|
exit;
|
|
}
|
|
|
|
// --- Route Handlers ---
|
|
|
|
function handleLogin(AdminAuth $auth): void
|
|
{
|
|
$error = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$username = trim($_POST['username'] ?? '');
|
|
$password = $_POST['password'] ?? '';
|
|
|
|
$result = $auth->login($username, $password);
|
|
if ($result['success']) {
|
|
header('Location: admin.php?route=dashboard');
|
|
exit;
|
|
}
|
|
$error = $result['message'];
|
|
}
|
|
|
|
require __DIR__ . '/../admin-console/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'];
|
|
|
|
$stats = [
|
|
'pages' => countFiles($contentDir, ['md', 'php', 'html']),
|
|
'directories' => countDirs($contentDir),
|
|
'plugins' => countDirs($pluginsDir),
|
|
'config_exists' => file_exists($configJson),
|
|
'content_size' => formatSize(dirSize($contentDir)),
|
|
'php_version' => PHP_VERSION,
|
|
];
|
|
|
|
// Load site config
|
|
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
|
|
|
require __DIR__ . '/../admin-console/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);
|
|
$fullPath = rtrim($contentDir, '/') . '/' . $subdir;
|
|
|
|
if (!is_dir($fullPath)) {
|
|
$fullPath = $contentDir;
|
|
$subdir = '';
|
|
}
|
|
|
|
$items = scanContentDir($fullPath, $subdir);
|
|
$route = 'content';
|
|
|
|
require __DIR__ . '/../admin-console/templates/layout.php';
|
|
}
|
|
|
|
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.php?route=content');
|
|
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($filePath, $content);
|
|
$message = 'Bestand opgeslagen.';
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
$fileContent = file_get_contents($filePath);
|
|
$fileName = basename($filePath);
|
|
$fileExt = pathinfo($filePath, PATHINFO_EXTENSION);
|
|
$route = 'content-edit';
|
|
|
|
require __DIR__ . '/../admin-console/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);
|
|
}
|
|
file_put_contents($filePath, $content);
|
|
header('Location: admin.php?route=content&dir=' . urlencode($dir));
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'content-new';
|
|
require __DIR__ . '/../admin-console/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)) {
|
|
unlink($filePath);
|
|
}
|
|
}
|
|
|
|
$dir = dirname($file);
|
|
header('Location: admin.php?route=content&dir=' . urlencode($dir === '.' ? '' : $dir));
|
|
exit;
|
|
}
|
|
|
|
function handleConfig(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$configJson = $config['config_json'];
|
|
$message = '';
|
|
$messageType = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
|
$message = 'Ongeldige CSRF token.';
|
|
$messageType = 'danger';
|
|
} else {
|
|
$jsonContent = $_POST['config_content'] ?? '';
|
|
$parsed = json_decode($jsonContent, true);
|
|
if ($parsed === null && json_last_error() !== JSON_ERROR_NONE) {
|
|
$message = 'Ongeldige JSON: ' . json_last_error_msg();
|
|
$messageType = 'danger';
|
|
} else {
|
|
file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
$message = 'Configuratie opgeslagen.';
|
|
$messageType = 'success';
|
|
$jsonContent = null; // reload from file
|
|
}
|
|
}
|
|
}
|
|
|
|
$siteConfig = file_exists($configJson) ? file_get_contents($configJson) : '{}';
|
|
if (isset($jsonContent)) {
|
|
$siteConfig = $jsonContent;
|
|
}
|
|
|
|
$route = 'config';
|
|
require __DIR__ . '/../admin-console/templates/layout.php';
|
|
}
|
|
|
|
function handlePlugins(AdminAuth $auth, array $config): void
|
|
{
|
|
$user = $auth->getCurrentUser();
|
|
$csrf = $auth->getCsrfToken();
|
|
$pluginsDir = $config['plugins_dir'];
|
|
$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');
|
|
|
|
$plugins[] = [
|
|
'name' => $item,
|
|
'path' => $pluginPath,
|
|
'enabled' => $pluginConfig['enabled'] ?? true,
|
|
'config' => $pluginConfig,
|
|
'has_config' => $hasConfig,
|
|
'has_main' => $hasMainFile,
|
|
'has_readme' => $hasReadme,
|
|
];
|
|
}
|
|
}
|
|
|
|
$route = 'plugins';
|
|
require __DIR__ . '/../admin-console/templates/layout.php';
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
$users = $auth->getUsers();
|
|
$route = 'users';
|
|
require __DIR__ . '/../admin-console/templates/layout.php';
|
|
}
|
|
|
|
// --- 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 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;
|
|
$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;
|
|
}
|