Files

540 lines
16 KiB
PHP
Raw Permalink Normal View History

2026-04-06 16:49:17 -04:00
<?php
namespace Vor\core;
use Exception;
use PDO;
use RangeException;
class Sys
{
// Public
public $userConfigs;
// Protected
protected static $conn = null;
// Methods
/**
* System enviroment handling
*/
public static function start(){
self::loadEnv();
$auth = self::hydrate('v_auth');
if($auth){
$_SERVER['VOR_AUTH'] = $auth;
}
return $auth;
}
public static function getUserConfigs(){
if (file_exists(BASE . '/rss/json/config/groups.json')) {
return json_decode(file_get_contents(BASE . '/rss/json/config/groups.json'), true);
} else {
return [];
}
}
private static function loadEnv(){
$orgPath = $_SERVER['SERVER_ADDR'] == '127.0.0.1' ? '/' : '/../../';
if(file_exists(BASE . $orgPath . '.env')) {
$lines = file(BASE . $orgPath . '.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (isset($lines) && !empty($lines)) {
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
$value = trim($value, '"\'');
putenv("$name=$value");
}
}
}else{
die('Env was not found at: ' . BASE . $orgPath . '.env');
}
}
/**
* Sys database methods.
* Don't edit this file. If you want to make system changes, extend from this class.
*/
public static function getConnection(){
if (!self::$conn) {
self::createConnection();
}
return self::$conn;
}
private static function createConnection(){
self::loadEnv();
if (self::validateDBEnv()) {
try {
$dbHost = getenv('DB_HOST');
$dbUser = getenv('DB_USER');
$dbPass = getenv('DB_PASS');
$dbName = getenv('DB_NAME');
self::$conn = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4;", $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => 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;
}
}