1) && (($value[0] === '"' && substr($value, -1) === '"') || ($value[0] === "'" && substr($value, -1) === "'"))) { $value = substr($value, 1, -1); } $_ENV[$key] = $value; putenv("$key=$value"); } } // Try loading .env from project root, then from one level up (outside public_html) loadEnv(__DIR__ . '/../.env'); loadEnv(dirname(__DIR__, 2) . '/.env'); // ── Helpers to read env values ─────────────────────────────── function env($key, $default = '') { return $_ENV[$key] ?? getenv($key) ?: $default; } // ── Error reporting ────────────────────────────────────────── $appEnv = env('APP_ENV', 'production'); if ($appEnv === 'production') { error_reporting(0); ini_set('display_errors', 0); } else { error_reporting(E_ALL); ini_set('display_errors', 1); } // Timezone date_default_timezone_set('UTC'); // ── Database Configuration ─────────────────────────────────── define('DB_TYPE', env('DB_TYPE', 'file')); define('DB_HOST', env('DB_HOST', 'localhost')); define('DB_NAME', env('DB_NAME', 'mspe_website')); define('DB_USER', env('DB_USER', '')); define('DB_PASS', env('DB_PASS', '')); // ── Admin credentials ──────────────────────────────────────── define('ADMIN_USER', env('ADMIN_USER', 'admin')); define('ADMIN_PASS', env('ADMIN_PASS', '')); // ── JWT Secret ─────────────────────────────────────────────── define('JWT_SECRET', env('JWT_SECRET', 'change-me-in-env-file')); if (JWT_SECRET === 'change-me-in-env-file' && $appEnv === 'production') { http_response_code(500); die(json_encode(['success' => false, 'message' => 'Server misconfigured'])); } // ── Data directory (for JSON files, cache, etc.) ──────────── define('DATA_DIR', __DIR__ . '/../data/'); // ── Upload settings ────────────────────────────────────────── define('UPLOAD_DIR', __DIR__ . '/../uploads/'); define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf']); // ── Site settings ──────────────────────────────────────────── define('SITE_NAME', env('SITE_NAME', 'MSPE')); define('SITE_URL', env('SITE_URL', 'https://mspe.pro')); define('ADMIN_EMAIL', env('ADMIN_EMAIL', 'info@mspe.pro')); // ── SMTP settings (read from .env, used as defaults before admin settings override) ── define('ENV_SMTP_HOST', env('SMTP_HOST', '')); define('ENV_SMTP_PORT', env('SMTP_PORT', '465')); define('ENV_SMTP_USER', env('SMTP_USER', '')); define('ENV_SMTP_PASS', env('SMTP_PASS', '')); define('ENV_SMTP_FROM_NAME', env('SMTP_FROM_NAME', 'MSPE')); define('ENV_SMTP_FROM_EMAIL', env('SMTP_FROM_EMAIL', 'info@mspe.pro')); // ── CORS Headers ───────────────────────────────────────────── $allowedOrigins = [SITE_URL, 'https://www.mspe.pro']; // Allow localhost for local development if ($appEnv !== 'production') { $allowedOrigins[] = 'http://localhost:8080'; $allowedOrigins[] = 'http://localhost:8000'; $allowedOrigins[] = 'http://127.0.0.1:8080'; } $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowedOrigins)) { header('Access-Control-Allow-Origin: ' . $origin); } else { header('Access-Control-Allow-Origin: ' . SITE_URL); } header('Vary: Origin'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); header('Access-Control-Allow-Credentials: true'); header('Content-Type: application/json; charset=UTF-8'); // Handle preflight requests if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit(); } // ── Security Headers ───────────────────────────────────────── header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('X-XSS-Protection: 0'); header('Referrer-Policy: strict-origin-when-cross-origin'); header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'"); header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()'); if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') { header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'); } /** * MySQL Database Helper */ class MySQLDB { private $pdo; public function __construct() { $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; $this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options); } /** Ensure the table exists (auto-create a generic key-value/json table) */ private function ensureTable($table) { $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); 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, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); } public function getAll($table) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $stmt = $this->pdo->query("SELECT `data` FROM `$safe` ORDER BY `created_at` ASC"); $rows = $stmt->fetchAll(); return array_map(function($r) { return json_decode($r['data'], true); }, $rows); } /** * Get records with SQL-level pagination (LIMIT/OFFSET). * Returns ['data' => [...], 'total' => int]. */ public function getAllPaginated($table, $limit = 50, $offset = 0, $orderDir = 'ASC') { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $dir = strtoupper($orderDir) === 'DESC' ? 'DESC' : 'ASC'; $total = (int)$this->pdo->query("SELECT COUNT(*) FROM `{$safe}`")->fetchColumn(); $stmt = $this->pdo->prepare("SELECT `data` FROM `{$safe}` ORDER BY `created_at` {$dir} LIMIT ? OFFSET ?"); $stmt->execute([(int)$limit, (int)$offset]); $rows = $stmt->fetchAll(); return [ 'data' => array_map(function($r) { return json_decode($r['data'], true); }, $rows), 'total' => $total ]; } public function get($table, $id) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $stmt = $this->pdo->prepare("SELECT `data` FROM `$safe` WHERE `id` = ? LIMIT 1"); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? json_decode($row['data'], true) : null; } public function insert($table, $data) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $data['id'] = $data['id'] ?? bin2hex(random_bytes(16)); $data['created_at'] = $data['created_at'] ?? date('Y-m-d H:i:s'); $data['updated_at'] = date('Y-m-d H:i:s'); $stmt = $this->pdo->prepare("INSERT INTO `$safe` (`id`, `data`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?)"); $stmt->execute([$data['id'], json_encode($data, JSON_UNESCAPED_UNICODE), $data['created_at'], $data['updated_at']]); return $data; } public function update($table, $id, $data) { $existing = $this->get($table, $id); if (!$existing) return null; $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $data['id'] = $id; $data['created_at'] = $existing['created_at']; $data['updated_at'] = date('Y-m-d H:i:s'); $merged = array_merge($existing, $data); $stmt = $this->pdo->prepare("UPDATE `$safe` SET `data` = ?, `updated_at` = ? WHERE `id` = ?"); $stmt->execute([json_encode($merged, JSON_UNESCAPED_UNICODE), $merged['updated_at'], $id]); return $merged; } public function delete($table, $id) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $stmt = $this->pdo->prepare("DELETE FROM `$safe` WHERE `id` = ?"); $stmt->execute([$id]); return true; } public function query($table, $conditions = []) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); if (empty($conditions)) { return $this->getAll($table); } // Use MySQL JSON_EXTRACT for server-side filtering when possible $where = []; $params = []; foreach ($conditions as $key => $value) { $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key); $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?"; $params[] = (string)$value; } $sql = "SELECT `data` FROM `{$safe}` WHERE " . implode(' AND ', $where); $stmt = $this->pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); return array_map(function($r) { return json_decode($r['data'], true); }, $rows); } /** Count rows matching conditions (avoids loading all data into memory) */ public function count($table, $conditions = []) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); if (empty($conditions)) { return (int)$this->pdo->query("SELECT COUNT(*) FROM `{$safe}`")->fetchColumn(); } $where = []; $params = []; foreach ($conditions as $key => $value) { $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key); $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?"; $params[] = (string)$value; } $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where); $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return (int)$stmt->fetchColumn(); } /** Count rows where a JSON date-like field is between two bounds (inclusive) */ public function countByDateRange($table, $dateField, $fromValue, $toValue, $conditions = []) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $safeDateField = preg_replace('/[^a-zA-Z0-9_]/', '', $dateField); $where = [ "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeDateField}')) >= ?", "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeDateField}')) <= ?" ]; $params = [(string)$fromValue, (string)$toValue]; foreach ($conditions as $key => $value) { $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key); $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?"; $params[] = (string)$value; } $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where); $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return (int)$stmt->fetchColumn(); } /** Count rows where a single JSON field matches a comparison operator */ public function countByFieldComparison($table, $field, $operator, $value, $conditions = []) { $this->ensureTable($table); $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table); $safeField = preg_replace('/[^a-zA-Z0-9_]/', '', $field); $allowedOps = ['=', '!=', '>', '>=', '<', '<=']; $op = in_array($operator, $allowedOps, true) ? $operator : '='; $where = ["JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeField}')) {$op} ?"]; $params = [(string)$value]; foreach ($conditions as $key => $condValue) { $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key); $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?"; $params[] = (string)$condValue; } $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where); $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return (int)$stmt->fetchColumn(); } } /** * Simple JWT Helper */ class JWT { public static function encode($payload, $ttlSeconds = 86400, $secret = JWT_SECRET) { $payload['exp'] = time() + max(300, (int)$ttlSeconds); return FirebaseJWT::encode($payload, $secret, 'HS256'); } public static function decode($token, $secret = JWT_SECRET) { try { $decoded = FirebaseJWT::decode($token, new Key($secret, 'HS256')); return (array) $decoded; } catch (Exception $e) { return null; } } } /** * Authentication Check */ function checkAuth() { // Prefer HttpOnly cookie token for admin authentication $cookieToken = $_COOKIE['mspe_admin_token'] ?? ''; if (is_string($cookieToken) && $cookieToken !== '') { $payload = JWT::decode($cookieToken); if ($payload) { return $payload; } } // Fallback: Authorization header (for API clients) $authHeader = ''; if (function_exists('getallheaders')) { $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? ''; } if ($authHeader === '' && isset($_SERVER['HTTP_AUTHORIZATION'])) { $authHeader = $_SERVER['HTTP_AUTHORIZATION']; } if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { $payload = JWT::decode($matches[1]); if ($payload) { return $payload; } } return null; } /** * Require Authentication */ function requireAuth() { $user = checkAuth(); if (!$user) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'Unauthorized']); exit(); } return $user; } /** * JSON Response Helper */ function jsonResponse($data, $code = 200) { http_response_code($code); ob_clean(); echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); die(); } /** * Sanitize Input (HTML Context) * Note: For JavaScript context, use json_encode(). * For URL context, use urlencode(). */ function sanitize($input) { if (is_array($input)) { return array_map('sanitize', $input); } // ENT_QUOTES | ENT_SUBSTITUTE handles both single/double quotes and invalid characters return htmlspecialchars(trim($input ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } /** * Sanitize Rich Text (HTML Context) * Allows only a whitelist of safe formatting tags and strips everything else. * Removes all event handler attributes (onclick, onerror, etc.) from allowed tags. */ function sanitizeRichText($input) { if (!is_string($input) || trim($input) === '') return ''; // Allow only safe formatting tags $allowed = '