Initial commit
This commit is contained in:
539
rss/php/classes/core/Sys.php
Normal file
539
rss/php/classes/core/Sys.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user