Fix icon hover colors, text alignment, and UI improvements
This commit is contained in:
+11
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user