PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_PERSISTENT => true // Remember to validate max connections ]); self::validateMigration(); } catch (Exception $e) { error_log($e->getMessage()); return false; } } else { return false; } } private static function validateDBEnv(){ return (getenv('DB_HOST') && getenv('DB_USER') && getenv('DB_PASS') && getenv('DB_NAME')); } private static function validateMigration(){ // Validate engine information try{ self::$conn->exec('CREATE TABLE IF NOT EXISTS engine_info ( id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, meta_key VARCHAR(95), meta_value TEXT )'); }catch(Exception $e){ // Could not initiate first query error_log($e->getMessage()); return; } // Validate or set engine version try{ $currentVersion = self::$conn->query('SELECT meta_value FROM engine_info WHERE meta_key = "vor_db_version"')->fetch(PDO::FETCH_COLUMN); if($currentVersion === false){ self::$conn->exec('INSERT INTO engine_info (meta_key, meta_value) VALUES ("vor_db_version", "0")'); $currentVersion = 0; } // Validate latest version if(file_exists(BASE . '/rss/json/config/migrations.json')){ $migrations = json_decode(file_get_contents(BASE . '/rss/json/config/migrations.json'), true); $latestVersion = array_key_last($migrations); if((int)$latestVersion > (int)$currentVersion){ self::migrate(); } } }catch(Exception $e){ // First query ran fine but something with the updates messes up error_log($e->getMessage()); } } private static function migrate(){ try{ $currentVersion = (int) self::$conn->query('SELECT meta_value FROM engine_info WHERE meta_key = "vor_db_version"')->fetch(PDO::FETCH_COLUMN) ?? 0; }catch(Exception $e){ // Could not get version error_log($e->getMessage()); } try{ $migrations = json_decode(file_get_contents(BASE . '/rss/json/config/migrations.json'), true); foreach($migrations as $index => $migration){ if((int)$index > (int)$currentVersion){ foreach($migration as $query){ self::$conn->exec($query); } $stmt = self::$conn->prepare("UPDATE engine_info SET meta_value = :index WHERE meta_key = 'vor_db_version'"); $stmt->bindValue(':index', (int)$index); $stmt->execute(); } } }catch(Exception $e){ // Updates failed but DB is fine error_log($e->getMessage()); } } /** * System utilies */ public static function post($key = null){ return $key === null ? $_POST : (isset($_POST[$key]) ? $_POST[$key] : null); } public static function get($key = null){ return $key === null ? $_GET : (isset($_GET[$key]) ? $_GET[$key] : null); } public static function go($path){ header('Location: ' . $path); exit(); } public static function json($data, $code = 200){ header('Content-Type: application/json'); http_response_code($code); echo json_encode($data); exit(); } public static function collect($json = false){ $input = file_get_contents('php://input'); if($json){ $input = json_decode($input, true); } return $input ?? []; } private static function isHttps(){ return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || (isset($_SERVER['SERVER_PORT']) && (int)$_SERVER['SERVER_PORT'] === 443); } public static function session($key, $value = null){ if(session_status() === PHP_SESSION_NONE){ self::validateSession(); } if($value === null){ return $_SESSION[$key] ?? null; } $_SESSION[$key] = $value; } public static function hydrate($cookieName = 'v_auth'){ $token = $_COOKIE[$cookieName] ?? false; if(!$token){ return false; } $parts = explode('.', $token, 2); if(count($parts) !== 2){ return false; } $payloadB64 = str_replace(['-', '_'], ['+', '/'], $parts[0]); $macB64 = str_replace(['-', '_'], ['+', '/'], $parts[1]); $payload = base64_decode($payloadB64, true); $mac = base64_decode($macB64, true); if(!$payload || !$mac){ return false; } $key = getenv('TOKEN_SECRET') ?? false; if(!$key){ return false; } $expectedMac = hash_hmac('sha256', $payload, $key, true); if(!hash_equals($expectedMac, $mac)){ return false; } $data = json_decode($payload, true); if(!is_array($data)){ return false; } $uid = isset($data['uid']) ? (int)$data['uid'] : 0; $exp = isset($data['exp']) ? (int)$data['exp'] : 0; $sid = isset($data['sid']) ? $data['sid'] : false; if($uid <= 0 || $exp <= 0 || !$sid){ return false; } if($exp < time()){ return false; } self::validateSession($sid); return $data; } public static function validateSession($sid = null){ if(session_status() !== PHP_SESSION_NONE){ return true; } ini_set('session.use_cookies', 0); ini_set('session.use_only_cookies', 0); if($sid){ session_id($sid); } return session_start(); } public static function cookieSet($name, $val, $opts = []){ $isHttps = self::isHttps(); if(is_array($val)){ $key = getenv('TOKEN_SECRET') ?? false; if(!$key){ return false; } $payload = json_encode($val, JSON_UNESCAPED_SLASHES); $mac = hash_hmac('sha256', $payload, $key, true); $safePayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); $safeMac = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($mac)); $val = $safePayload . '.' . $safeMac; } $defaults = [ 'expires' => 0, 'path' => '/', 'domain' => '', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Lax' ]; $opts = array_intersect_key((array)$opts, array_flip(['expires', 'path', 'domain', 'secure', 'httponly', 'samesite'])); $final = array_merge($defaults, $opts); if(strcasecmp($final['samesite'], 'None') === 0){ $final['secure'] = true; } $success = setcookie($name, (string)$val, $final); if($success){ $_COOKIE[$name] = (string)$val; } return $success; } public static function cookieClear($name = 0){ $isHttps = self::isHttps(); $clear = function($cookie) use ($isHttps){ setcookie($cookie, '', [ 'expires' => time() - 3600, 'path' => '/', 'secure' => $isHttps, 'httponly' => true, 'samesite' => 'Lax' ]); unset($_COOKIE[$cookie]); }; if(!$name){ foreach($_COOKIE as $cookie => $v){ $clear($cookie); } return true; } $clear($name); return true; } public static function upload($preset, $file){ Sys::loadEnv(); if(!isset($file['error']) || is_array($file['error'])){ return ['ok' => false, 'error' => 'Invalid upload']; } if($file['error'] !== UPLOAD_ERR_OK){ return ['ok' => false, 'error' => 'Upload failed']; } $dir = getenv("UPLOAD_{$preset}_DIR"); $allowed = getenv("UPLOAD_{$preset}_ALLOWED"); $maxMb = getenv("UPLOAD_{$preset}_MAX_MB"); if(!$dir || !$allowed){ return ['ok' => false, 'error' => 'Upload preset missing']; } $allowedExt = array_map('trim', explode(',', strtolower($allowed))); $maxBytes = (int)$maxMb * 1024 * 1024; if($maxBytes > 0 && $file['size'] > $maxBytes){ return ['ok' => false, 'error' => 'File too large']; } $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if(!in_array($ext, $allowedExt)){ return ['ok' => false, 'error' => 'Invalid file type']; } $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); if(!$mime){ return ['ok' => false, 'error' => 'Invalid file']; } $absDir = rtrim(BASE, '/') . '/' . ltrim($dir, '/'); if(!is_dir($absDir)){ mkdir($absDir, 0755, true); } $name = bin2hex(random_bytes(32)) . '.' . $ext; $dest = rtrim($absDir, '/') . '/' . $name; if(!move_uploaded_file($file['tmp_name'], $dest)){ return ['ok' => false, 'error' => 'Move failed']; } return[ 'ok' => true, 'path' => $dir . '/' . $name, 'filename' => $name, 'ext' => $ext, 'mime' => $mime, 'size' => $file['size'] ]; } public static function serveFile($absPath, $opts = []){ $defaults = [ 'filename' => null, 'mime' => null, 'inline' => true, 'cacheSeconds' => 86400, 'etag' => true, 'allowRange' => true, 'baseDirs' => [realpath(BASE)], ]; $o = array_merge($defaults, (array)$opts); $real = realpath($absPath); if($real === false || !is_file($real) || !is_readable($real)){ http_response_code(404); exit; } $allowed = false; foreach((array)$o['baseDirs'] as $base){ $baseReal = realpath($base); if($baseReal && str_starts_with($real, rtrim($baseReal, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR)){ $allowed = true; break; } } if(!$allowed){ http_response_code(404); exit; } $mime = $o['mime']; if(!$mime){ $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($real) ?: 'application/octet-stream'; } $size = filesize($real); $mtime = filemtime($real); if($o['etag']){ $etag = '"' . sha1($real . '|' . $size . '|' . $mtime) . '"'; header('ETag: ' . $etag); if(isset($_SERVER['HTTP_IF_NONE_MATCH']) &&trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag){ http_response_code(304); exit; } } if((int)$o['cacheSeconds'] > 0){ header('Cache-Control: public, max-age=' . (int)$o['cacheSeconds']); }else{ header('Cache-Control: no-store'); } header('X-Content-Type-Options: nosniff'); header('Content-Type: ' . $mime); $downloadName = $o['filename'] ?: basename($real); $downloadName = str_replace(["\r","\n"], '', $downloadName); $disp = $o['inline'] ? 'inline' : 'attachment'; header('Content-Disposition: ' . $disp . '; filename="' . addslashes($downloadName) . '"'); header('Accept-Ranges: bytes'); $start = 0; $end = $size - 1; $status = 200; if($o['allowRange'] && isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/i', $_SERVER['HTTP_RANGE'], $m)){ $rangeStart = $m[1] !== '' ? (int)$m[1] : null; $rangeEnd = $m[2] !== '' ? (int)$m[2] : null; if($rangeStart === null && $rangeEnd !== null){ $start = max(0, $size - $rangeEnd); }else{ $start = max(0, (int)$rangeStart); if($rangeEnd !== null){ $end = min($end, (int)$rangeEnd); } } if($start > $end || $start >= $size){ header('Content-Range: bytes */' . $size); http_response_code(416); exit; } $status = 206; header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); } $length = ($end - $start) + 1; header('Content-Length: ' . $length); http_response_code($status); $fp = fopen($real, 'rb'); if($fp === false){ http_response_code(500); exit(); } if($start > 0){ fseek($fp, $start); } $chunk = 8192; while(!feof($fp) && $length > 0){ $read = ($length > $chunk) ? $chunk : $length; $buf = fread($fp, $read); if($buf === false){ break; } echo $buf; $length -= strlen($buf); if(function_exists('fastcgi_finish_request') === false){ flush(); } } fclose($fp); exit; } }