Initial commit

This commit is contained in:
2026-04-06 16:49:17 -04:00
commit 7ec75d0747
36 changed files with 14526 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
<?php
namespace Vor\core;
use PDO;
use Exception;
use Vor\core\Sys;
class Main{
/**
* Static query functions
*/
public static function insert($table, $data, $whitelist = []){
if(!self::assertIdent($table)){
throw new \InvalidArgumentException('Invalid table name');
}
$db = Sys::getConnection();
if(!empty($whitelist)){
$data = array_intersect_key($data, array_flip($whitelist));
}
if(empty($data)){
return false;
}
$fields = array_keys($data);
$cols = implode(', ', $fields);
$placeholders = ':' . implode(', :' , $fields);
$query = "INSERT INTO $table ($cols) VALUES ($placeholders)";
try{
$stmt = $db->prepare($query);
foreach($data as $key => $val){
$stmt->bindValue(':' . $key, $val);
}
if($stmt->execute()){
return $db->lastInsertId();
}
}catch(Exception $e) {
error_log($e->getMessage());
return false;
}
}
public static function update($table, $data, $where, $whitelist = []){
if(!self::assertIdent($table)){
throw new \InvalidArgumentException('Invalid table name');
}
$db = Sys::getConnection();
if(!empty($whitelist)){
$data = array_intersect_key($data, array_flip($whitelist));
}
if(empty($data)){
return false;
}
$setParts = [];
$whereParts = [];
// Set
foreach($data as $key => $val){
$setParts[] = "$key = :$key";
}
$setSql = implode(', ', $setParts);
// Where
foreach($where as $key => $val){
$whereParts[] = "$key = :w_$key";
}
$whereSql = implode(' AND ', $whereParts);
$query = "UPDATE $table SET $setSql WHERE $whereSql";
try{
$stmt = $db->prepare($query);
foreach($data as $key => $val){
$stmt->bindValue(':' . $key, $val);
}
foreach($where as $key => $val){
$stmt->bindValue(':w_' . $key, $val);
}
return $stmt->execute();
}catch(Exception $e){
error_log($e->getMessage());
return false;
}
}
public static function select($table, $list = [], $where = [], $multiple = false, $assoc = true){
if(!self::assertIdent($table)){
throw new \InvalidArgumentException('Invalid table name');
}
foreach($list as $col){
if(!self::assertIdent($col)){
throw new \InvalidArgumentException('Invalid where name');
}
}
foreach($where as $k => $_){
if(!self::assertIdent($k)){
throw new \InvalidArgumentException('Invalid where key');
}
}
$db = Sys::getConnection();
if(empty($list)){
$query = "SELECT * FROM $table";
}else{
$querySelect = implode(', ', $list);
$query = "SELECT $querySelect FROM $table";
}
if(!empty($where)){
$whereParts = [];
foreach($where as $key => $val){
$whereParts[] = "$key = :w_$key";
}
$whereSql = implode(' AND ', $whereParts);
$query .= " WHERE $whereSql";
}
try{
$stmt = $db->prepare($query);
foreach($where as $key => $val){
$stmt->bindValue(':w_' . $key, $val);
}
if($stmt->execute()){
$mode = $assoc ? PDO::FETCH_ASSOC : PDO::FETCH_COLUMN;
return $multiple ? $stmt->fetchAll($mode) : $stmt->fetch($mode);
}
return false;
}catch(Exception $e){
error_log($e->getMessage());
return false;
}
}
public static function query($sql, $params = [], $multiple = true, $assoc = true){
$db = Sys::getConnection();
try{
$stmt = $db->prepare($sql);
if(!empty($params)){
foreach($params as $key => $val){
$stmt->bindValue(':' . $key, $val);
}
}
if($stmt->execute()){
$mode = $assoc ? PDO::FETCH_ASSOC : PDO::FETCH_COLUMN;
return $multiple ? $stmt->fetchAll($mode) : $stmt->fetch($mode);
}
return false;
}catch(Exception $e){
error_log($e->getMessage());
return false;
}
}
public static function curl($url, $data = [], $headers = []){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if(!empty($data)){
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$headers[] = 'Content-Type: application/json';
}
if(!empty($headers)){
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
try{
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
if($error){
error_log('Curl erro: ' . $error);
return false;
}
return [
'status' => $httpCode,
'body' => json_decode($response, true) ?? $response
];
}catch(Exception $e){
error_log($e->getMessage());
return false;
}
}
public static function assertIdent($s){
return is_string($s) && preg_match('/^[a-zA-Z0-9_]+$/', $s);
}
}

View File

@@ -0,0 +1,539 @@
<?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;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Vor\core;
class Validator{
public static function int($val) : bool{
return filter_var($val, FILTER_VALIDATE_INT) !== false;
}
public static function float($val) : bool{
return filter_var($val, FILTER_VALIDATE_FLOAT) !== false;
}
public static function string($val, int $minLen = 1): bool{
if(!is_string($val)){
return false;
}
return mb_strlen(trim($val)) >= $minLen;
}
public static function date($date, string $format = 'Y-m-d'): bool {
try{
$d = \DateTime::createFromFormat($format, (string)$date);
return $d && $d->format($format) === (string)$date;
}catch(\Throwable $e){
error_log($e->getMessage());
return false;
}
}
public static function json($val): bool{
if(!is_string($val) || $val === ''){
return false;
}
try{
json_decode($val, true, 512, JSON_THROW_ON_ERROR);
return true;
}catch(\JsonException $e){
error_log($e->getMessage());
return false;
}
}
public static function email($val): bool{
$val = trim((string)$val);
if(preg_match("/[\r\n]/", $val)){
return false;
}
return (bool) filter_var($val, FILTER_VALIDATE_EMAIL);
}
public static function domain($val): bool{
$val = trim((string)$val);
if(preg_match("/[\r\n]/", $val)){
return false;
}
return (bool) filter_var($val, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
}
public static function minLen($val, int $min): bool{
return mb_strlen(trim((string)$val)) >= $min;
}
public static function maxLen($val, int $max): bool{
return mb_strlen(trim((string)$val)) <= $max;
}
public static function regex($val, string $pattern): bool{
return (bool) preg_match($pattern, (string)$val);
}
}