PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . DB_CHARSET, ]; try { $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); } catch (PDOException $e) { if (DEBUG_MODE) { throw $e; } error_log('Database connection failed: ' . $e->getMessage()); throw new PDOException('Database connection failed'); } } return $pdo; } /** * Set CORS headers for API responses */ function setCorsHeaders(): void { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, CORS_ALLOWED_ORIGINS)) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Credentials: true'); } elseif (DEBUG_MODE && str_starts_with($origin, 'http://127.0.0.1:')) { header("Access-Control-Allow-Origin: $origin"); header('Access-Control-Allow-Credentials: true'); } // Neznamy origin = zadny CORS header header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('Access-Control-Max-Age: 86400'); // Handle preflight requests if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit(); } } /** * Send JSON response and exit * * @param mixed $data Data to send * @param int $statusCode HTTP status code */ function jsonResponse($data, int $statusCode = 200): void { http_response_code($statusCode); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit(); } /** * Send error response * * @param string $message Error message * @param int $statusCode HTTP status code */ function errorResponse(string $message, int $statusCode = 400): void { jsonResponse(['success' => false, 'error' => $message], $statusCode); } /** * Send success response * * @param mixed $data Data to include * @param string $message Optional message */ function successResponse($data = null, string $message = ''): void { $response = ['success' => true]; if ($message) { $response['message'] = $message; } if ($data !== null) { $response['data'] = $data; } jsonResponse($response); } /** * Get JSON request body * * @return array Decoded JSON data */ function getJsonInput(): array { $input = file_get_contents('php://input'); $data = json_decode($input, true); return is_array($data) ? $data : []; } /** * Sanitize string input * * @param string $input Input string * @return string Sanitized string */ function sanitize(string $input): string { return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8'); } /** * Validate email format * * @param string $email Email to validate * @return bool True if valid */ function isValidEmail(string $email): bool { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; } /** * Validate and sanitize month parameter (YYYY-MM format) */ function validateMonth(string $param = 'month'): string { $month = $_GET[$param] ?? date('Y-m'); if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { $month = date('Y-m'); } return $month; } /** * Get client IP address * * Uses only REMOTE_ADDR which cannot be spoofed (TCP connection IP). * If you add a reverse proxy (Cloudflare, Nginx, etc.) in the future, * update this function to trust specific proxy headers only from known proxy IPs. * * @return string IP address */ function getClientIp(): string { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * Set security headers for API responses * * Sets standard security headers to protect against common web vulnerabilities: * - X-Content-Type-Options: Prevents MIME type sniffing * - X-Frame-Options: Prevents clickjacking attacks * - X-XSS-Protection: Enables browser XSS filter * - Referrer-Policy: Controls referrer information sent with requests * * Note: Content-Security-Policy is not set here as it may interfere with the React frontend */ function setSecurityHeaders(): void { header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Referrer-Policy: strict-origin-when-cross-origin'); if (!DEBUG_MODE && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } } /** * Set no-cache headers * * Prevents browser caching for sensitive endpoints */ function setNoCacheHeaders(): void { header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); } /** * Sdilene generovani cisel pro objednavky a projekty (spolecny ciselny prostor) */ function generateSharedNumber(PDO $pdo): string { $yy = date('y'); $settings = $pdo->query('SELECT order_type_code FROM company_settings LIMIT 1')->fetch(); $typeCode = ($settings && !empty($settings['order_type_code'])) ? $settings['order_type_code'] : '71'; $prefix = $yy . $typeCode; $prefixLen = strlen($prefix); $likePattern = $prefix . '%'; $stmt = $pdo->prepare(' SELECT COALESCE(MAX(seq), 0) FROM ( SELECT CAST(SUBSTRING(order_number, ? + 1) AS UNSIGNED) AS seq FROM orders WHERE order_number LIKE ? UNION ALL SELECT CAST(SUBSTRING(project_number, ? + 1) AS UNSIGNED) AS seq FROM projects WHERE project_number LIKE ? ) combined '); $stmt->execute([$prefixLen, $likePattern, $prefixLen, $likePattern]); $max = (int) $stmt->fetchColumn(); return sprintf('%s%s%04d', $yy, $typeCode, $max + 1); } /** * Get permissions for a user by their ID * Cached per-request via static variable * * @return list */ function getUserPermissions(int $userId): array { static $cache = []; if (isset($cache[$userId])) { return $cache[$userId]; } try { $pdo = db(); $stmt = $pdo->prepare(' SELECT r.name FROM users u JOIN roles r ON u.role_id = r.id WHERE u.id = ? '); $stmt->execute([$userId]); $role = $stmt->fetch(); if ($role && $role['name'] === 'admin') { $stmt = $pdo->query('SELECT name FROM permissions'); $cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN); return $cache[$userId]; } $stmt = $pdo->prepare(' SELECT p.name FROM permissions p JOIN role_permissions rp ON p.id = rp.permission_id JOIN users u ON u.role_id = rp.role_id WHERE u.id = ? '); $stmt->execute([$userId]); $cache[$userId] = $stmt->fetchAll(PDO::FETCH_COLUMN); return $cache[$userId]; } catch (PDOException $e) { error_log('getUserPermissions error: ' . $e->getMessage()); return []; } } /** * Require a specific permission, return 403 if denied * * @param array $authData */ function requirePermission(array $authData, string $permission): void { if ($authData['user']['is_admin'] ?? false) { return; } $permissions = getUserPermissions($authData['user_id']); if (!in_array($permission, $permissions)) { errorResponse('Přístup odepřen. Nemáte potřebná oprávnění.', 403); } } /** * Check if user has a specific permission (returns bool) * * @param array $authData */ function hasPermission(array $authData, string $permission): bool { if ($authData['user']['is_admin'] ?? false) { return true; } $permissions = getUserPermissions($authData['user_id']); return in_array($permission, $permissions); }