Fix icon hover colors, text alignment, and UI improvements

This commit is contained in:
Krikorios
2026-02-25 23:25:59 +02:00
parent 00fda3e045
commit fcc4352afa
46 changed files with 4011 additions and 600 deletions
+11 -4
View File
@@ -10,6 +10,9 @@ $body = getRequestBody();
switch ($method) {
case 'POST':
// Add CSRF protection for auth endpoints
requireSameOriginRequest();
$action = $body['action'] ?? 'login';
if ($action === 'login') {
@@ -82,19 +85,18 @@ function handleLogin($body) {
}
}
recordFailedLogin();
recordFailedLogin($username);
auditLog('login_failure', ['username' => $username]);
jsonResponse(['success' => false, 'message' => 'Invalid credentials'], 401);
}
function recordFailedLogin() {
function recordFailedLogin($username = '') {
global $db;
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$body = getRequestBody();
$db->insert('login_attempts', [
'ip_address' => $ip,
'username' => sanitize($body['username'] ?? ''),
'username' => sanitize($username),
'attempted_at' => date('Y-m-d H:i:s')
]);
@@ -236,6 +238,11 @@ function requestPasswordReset($body) {
jsonResponse(['success' => false, 'message' => 'Email or username is required'], 400);
}
// Validate email format if it looks like an email
if (strpos($identity, '@') !== false && !filter_var($identity, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => true, 'message' => 'If an account exists, a reset email has been sent.']);
}
$matched = findUserForPasswordReset($identity);
// Always return generic success to avoid username/email enumeration
+59 -1
View File
@@ -268,6 +268,25 @@ function blockTime() {
jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400);
}
// Validate date format
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$data['date'])) {
jsonResponse(['success' => false, 'message' => 'Invalid date format (use YYYY-MM-DD)'], 400);
}
// Validate time format
$time = (string)$data['time'];
if (!preg_match('/^\d{2}:\d{2}$/', $time)) {
jsonResponse(['success' => false, 'message' => 'Invalid time format (use HH:MM)'], 400);
}
// Validate hour and minute
list($hour, $minute) = explode(':', $time);
$hour = (int)$hour;
$minute = (int)$minute;
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
jsonResponse(['success' => false, 'message' => 'Invalid time values'], 400);
}
$blockData = [
'date' => sanitize($data['date']),
'time' => sanitize($data['time']),
@@ -332,10 +351,35 @@ function createAvailabilitySlot() {
jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400);
}
// Validate date format
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', (string)$data['date'])) {
jsonResponse(['success' => false, 'message' => 'Invalid date format (use YYYY-MM-DD)'], 400);
}
// Validate time format
$time = (string)$data['time'];
if (!preg_match('/^\d{2}:\d{2}$/', $time)) {
jsonResponse(['success' => false, 'message' => 'Invalid time format (use HH:MM)'], 400);
}
// Validate hour and minute
list($hour, $minute) = explode(':', $time);
$hour = (int)$hour;
$minute = (int)$minute;
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
jsonResponse(['success' => false, 'message' => 'Invalid time values'], 400);
}
// Validate capacity
$capacity = (int)($data['capacity'] ?? 1);
if ($capacity < 1 || $capacity > 100) {
jsonResponse(['success' => false, 'message' => 'Capacity must be between 1 and 100'], 400);
}
$slotData = [
'date' => sanitize($data['date']),
'time' => sanitize($data['time']),
'capacity' => (int)($data['capacity'] ?? 1),
'capacity' => $capacity,
'is_available' => true,
'notes' => sanitize($data['notes'] ?? '')
];
@@ -380,6 +424,20 @@ function createBooking($isPublicRequest = false) {
jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400);
}
// Validate time format (HH:MM)
$bookingTime = (string)$data['booking_time'];
if (!preg_match('/^\d{2}:\d{2}$/', $bookingTime)) {
jsonResponse(['success' => false, 'message' => 'Invalid time format (use HH:MM)'], 400);
}
// Validate hour and minute values
list($hour, $minute) = explode(':', $bookingTime);
$hour = (int)$hour;
$minute = (int)$minute;
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
jsonResponse(['success' => false, 'message' => 'Invalid time values'], 400);
}
$bookingDate = date('Y-m-d', strtotime($data['booking_date']));
$today = date('Y-m-d');
+88 -17
View File
@@ -148,7 +148,10 @@ class MySQLDB {
/** Ensure the table exists (auto-create a generic key-value/json table) */
private function ensureTable($table) {
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
$this->pdo->exec("CREATE TABLE IF NOT EXISTS `$safe` (
if ($safe === '' || strlen($safe) > 64) {
throw new Exception('Invalid table name');
}
$this->pdo->exec("CREATE TABLE IF NOT EXISTS `{$safe}` (
`id` VARCHAR(64) NOT NULL PRIMARY KEY,
`data` JSON NOT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -390,8 +393,9 @@ function requireAuth() {
*/
function jsonResponse($data, $code = 200) {
http_response_code($code);
echo json_encode($data);
exit();
ob_clean();
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
die();
}
/**
@@ -449,6 +453,8 @@ function requireSameOriginRequest() {
$allowedOrigins[] = 'http://localhost:8080';
$allowedOrigins[] = 'http://localhost:8000';
$allowedOrigins[] = 'http://127.0.0.1:8080';
$allowedOrigins[] = 'http://localhost';
$allowedOrigins[] = 'http://127.0.0.1';
}
$origin = trim((string)($_SERVER['HTTP_ORIGIN'] ?? ''));
@@ -469,6 +475,11 @@ function requireSameOriginRequest() {
jsonResponse(['success' => false, 'message' => 'Forbidden referer'], 403);
}
// In development mode, allow requests without Origin/Referer headers (e.g., for curl testing)
if ($appEnv !== 'production') {
return;
}
// Browser request with neither header is suspicious for public form submissions.
jsonResponse(['success' => false, 'message' => 'CSRF validation failed'], 403);
}
@@ -503,12 +514,29 @@ function getSetting($key, $default = null) {
* Send email (mail() or SMTP based on admin settings)
*/
function sendEmail($to, $subject, $htmlBody, $plainBody = '', $replyTo = null) {
// Validate required parameters
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
return ['success' => false, 'message' => 'Invalid recipient email address'];
}
if (trim((string)$subject) === '') {
return ['success' => false, 'message' => 'Subject cannot be empty'];
}
if (trim((string)$htmlBody) === '') {
return ['success' => false, 'message' => 'Email body cannot be empty'];
}
if (!empty($replyTo) && !filter_var($replyTo, FILTER_VALIDATE_EMAIL)) {
return ['success' => false, 'message' => 'Invalid reply-to email address'];
}
// Determine transport: admin setting → auto-detect from .env
$defaultTransport = (ENV_SMTP_HOST !== '') ? 'smtp' : 'mail';
$transport = strtolower((string)getSetting('email_transport', $defaultTransport));
$fromEmail = trim((string)getSetting('smtp_from_email', ENV_SMTP_FROM_EMAIL ?: ADMIN_EMAIL));
if ($fromEmail === '') {
if ($fromEmail === '' || !filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) {
$fromEmail = ADMIN_EMAIL;
}
@@ -578,34 +606,60 @@ function sendEmailViaSmtp($payload) {
$port = (int)$payload['port'];
$encryption = $payload['encryption'];
$remote = $encryption === 'ssl' ? 'ssl://' . $host : $host;
$socket = @stream_socket_client($remote . ':' . $port, $errno, $errstr, 20, STREAM_CLIENT_CONNECT);
if (!$socket) {
return ['success' => false, 'message' => 'SMTP connect failed: ' . $errstr];
// Validate port range
if ($port < 1 || $port > 65535) {
return ['success' => false, 'message' => 'Invalid SMTP port'];
}
stream_set_timeout($socket, 20);
$remote = $encryption === 'ssl' ? 'ssl://' . $host : $host;
$errno = 0;
$errstr = '';
$socket = @stream_socket_client($remote . ':' . $port, $errno, $errstr, 30, STREAM_CLIENT_CONNECT);
if (!$socket) {
return ['success' => false, 'message' => 'SMTP connect failed: ' . ($errstr ?: 'Unknown error')];
}
stream_set_timeout($socket, 30);
$expect = function($codes) use ($socket) {
$response = '';
$timeout = false;
while (($line = fgets($socket, 515)) !== false) {
$response .= $line;
if (preg_match('/^\d{3}\s/', $line)) {
break;
}
}
// Check for timeout
if ($line === false) {
$metadata = stream_get_meta_data($socket);
if (isset($metadata['timed_out']) && $metadata['timed_out']) {
throw new Exception('SMTP timeout - no response from server');
}
}
if ($response === '') {
throw new Exception('No response from SMTP server');
}
$code = (int)substr($response, 0, 3);
if (!in_array($code, (array)$codes, true)) {
throw new Exception(trim($response));
throw new Exception(trim($response ?: 'Unknown SMTP error'));
}
return $response;
};
$send = function($command) use ($socket) {
fwrite($socket, $command . "\r\n");
if (!is_resource($socket)) {
throw new Exception('Socket connection lost');
}
$bytes = fwrite($socket, $command . "\r\n");
if ($bytes === false) {
throw new Exception('Failed to write to socket');
}
};
try {
@@ -662,16 +716,27 @@ function sendEmailViaSmtp($payload) {
$message .= $payload['html'] . "\r\n\r\n";
$message .= "--{$boundary}--\r\n.";
fwrite($socket, $message . "\r\n");
if (!is_resource($socket)) {
throw new Exception('Socket connection lost before sending message');
}
$bytes = fwrite($socket, $message . "\r\n");
if ($bytes === false) {
throw new Exception('Failed to send message to SMTP server');
}
$expect([250]);
$send('QUIT');
fclose($socket);
if (is_resource($socket)) {
fclose($socket);
}
return ['success' => true, 'message' => 'Sent via SMTP'];
} catch (Exception $e) {
fclose($socket);
return ['success' => false, 'message' => 'SMTP send failed: ' . $e->getMessage()];
if (is_resource($socket)) {
fclose($socket);
}
error_log('MSPE SMTP error: ' . $e->getMessage());
return ['success' => false, 'message' => 'Email delivery failed'];
}
}
@@ -726,7 +791,13 @@ function handleFileUpload($file, $subdir = '') {
// Create upload directory
$uploadDir = UPLOAD_DIR . ($subdir ? $subdir . '/' : '');
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
if (!mkdir($uploadDir, 0755, true)) {
return ['success' => false, 'message' => 'Failed to create upload directory'];
}
}
if (!is_writable($uploadDir)) {
return ['success' => false, 'message' => 'Upload directory is not writable'];
}
// Generate unique filename (cryptographically random)
+10 -1
View File
@@ -156,10 +156,19 @@ function submitMessage() {
}
// Validate email
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$email = trim((string)($data['email'] ?? ''));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400);
}
// Validate phone format if provided (basic check)
if (!empty($data['phone'])) {
$phone = preg_replace('/[^\d+\-() ]/', '', (string)$data['phone']);
if (strlen($phone) < 5) {
jsonResponse(['success' => false, 'message' => 'Invalid phone number'], 400);
}
}
// Honeypot check (if implemented in form)
if (!empty($data['website'])) {
// Likely a bot
+11 -2
View File
@@ -145,10 +145,19 @@ function deleteMediaInternal($id) {
$item = $db->get('media', $id);
if ($item) {
// Delete physical file
// Delete physical file with path traversal protection
$filepath = __DIR__ . '/../' . $item['path'];
if (file_exists($filepath)) {
unlink($filepath);
// Verify path is within uploads directory (prevent directory traversal)
$uploadDir = realpath(UPLOAD_DIR);
$filePath = realpath($filepath);
if ($filePath && $uploadDir && strpos($filePath, $uploadDir) === 0) {
if (!unlink($filepath)) {
error_log('MSPE: Failed to delete file ' . $filepath);
}
} else {
error_log('MSPE: Path traversal attempt detected in deleteMedia');
}
}
$db->delete('media', $id);
return true;