4in1_ws_web/engine/request.php

239 lines
8.7 KiB
PHP

<?php
enum HTTPCode: int {
case MovedPermanently = 301;
case Found = 302;
case InvalidRequest = 400;
case Unauthorized = 401;
case NotFound = 404;
case Forbidden = 403;
case InternalServerError = 500;
case NotImplemented = 501;
}
enum InputVarType: string {
case INTEGER = 'i';
case FLOAT = 'f';
case BOOLEAN = 'b';
case STRING = 's';
case ENUM = 'e';
}
//function ensureAdmin() {
// if (!isAdmin())
// forbidden();
// $this->skin->setRenderOptions(['inside_admin_interface' => true]);
//}
abstract class request_handler {
protected array $routerInput = [];
protected skin $skin;
public static function resolveAndDispatch() {
if (!in_array($_SERVER['REQUEST_METHOD'], ['POST', 'GET']))
self::httpError(HTTPCode::NotImplemented, 'Method '.$_SERVER['REQUEST_METHOD'].' not implemented');
$uri = $_SERVER['REQUEST_URI'];
if (($pos = strpos($uri, '?')) !== false)
$uri = substr($uri, 0, $pos);
$router = router::getInstance();
$route = $router->find($uri);
if ($route === null)
self::httpError(HTTPCode::NotFound, 'Route not found');
$route = preg_split('/ +/', $route);
$handler_class = $route[0].'Handler';
if (!class_exists($handler_class))
self::httpError(HTTPCode::NotFound, isDev() ? 'Handler class "'.$handler_class.'" not found' : '');
$action = $route[1];
$input = [];
if (count($route) > 2) {
for ($i = 2; $i < count($route); $i++) {
$var = $route[$i];
list($k, $v) = explode('=', $var);
$input[trim($k)] = trim($v);
}
}
/** @var request_handler $handler */
$handler = new $handler_class();
$handler->callAct($_SERVER['REQUEST_METHOD'], $action, $input);
}
public function __construct() {
$this->skin = skin::getInstance();
$this->skin->addStatic(
'css/common.css',
'js/common.js'
);
$this->skin->setGlobal([
'is_admin' => isAdmin(),
'is_dev' => isDev()
]);
}
public function beforeDispatch(string $http_method, string $action) {}
public function callAct(string $http_method, string $action, array $input = []) {
$handler_method = $_SERVER['REQUEST_METHOD'].'_'.$action;
if (!method_exists($this, $handler_method))
$this->notFound(static::class.'::'.$handler_method.' is not defined');
if (!((new ReflectionMethod($this, $handler_method))->isPublic()))
$this->notFound(static::class.'::'.$handler_method.' is not public');
if (!empty($input))
$this->routerInput += $input;
$args = $this->beforeDispatch($http_method, $action);
return call_user_func_array([$this, $handler_method], is_array($args) ? [$args] : []);
}
public function input(string $input, array $options = []): array {
$options = array_merge(['trim' => false], $options);
$strval = fn(mixed $val): string => $options['trim'] ? trim((string)$val) : (string)$val;
$input = preg_split('/,\s+?/', $input, -1, PREG_SPLIT_NO_EMPTY);
$ret = [];
foreach ($input as $var) {
$enum_values = null;
$enum_default = null;
$pos = strpos($var, ':');
if ($pos === 1) { // only one-character type specifiers are supported
$type = substr($var, 0, $pos);
$rest = substr($var, $pos + 1);
$vartype = InputVarType::tryFrom($type);
if (is_null($vartype))
self::internalServerError('invalid input type '.$type);
if ($vartype == InputVarType::ENUM) {
$br_from = strpos($rest, '(');
$br_to = strpos($rest, ')');
if ($br_from === false || $br_to === false)
self::internalServerError('failed to parse enum values: '.$rest);
$enum_values = array_map('trim', explode('|', trim(substr($rest, $br_from + 1, $br_to - $br_from - 1))));
$name = trim(substr($rest, 0, $br_from));
if (!empty($enum_values)) {
foreach ($enum_values as $key => $val) {
if (str_starts_with($val, '=')) {
$enum_values[$key] = substr($val, 1);
$enum_default = $enum_values[$key];
}
}
}
} else {
$name = trim($rest);
}
} else {
$vartype = InputVarType::STRING;
$name = trim($var);
}
$val = null;
if (isset($this->routerInput[$name])) {
$val = $this->routerInput[$name];
} else if (isset($_POST[$name])) {
$val = $_POST[$name];
} else if (isset($_GET[$name])) {
$val = $_GET[$name];
}
if (is_array($val))
$val = $strval(implode($val));
$ret[] = match($vartype) {
InputVarType::INTEGER => (int)$val,
InputVarType::FLOAT => (float)$val,
InputVarType::BOOLEAN => (bool)$val,
InputVarType::ENUM => !in_array($val, $enum_values) ? $enum_default ?? '' : $strval($val),
default => $strval($val)
};
}
return $ret;
}
public function getPage(int $per_page, ?int $count = null): array {
list($page) = $this->input('i:page');
$pages = $count !== null ? ceil($count / $per_page) : null;
if ($pages !== null && $page > $pages)
$page = $pages;
if ($page < 1)
$page = 1;
$offset = $per_page * ($page-1);
return [$page, $pages, $offset];
}
protected static function ensureXhr(): void {
if (!self::isXhrRequest())
self::invalidRequest();
}
public static function getCSRF(string $key): string {
global $config;
$user_key = isAdmin() ? admin::getCSRFSalt() : $_SERVER['REMOTE_ADDR'];
return substr(hash('sha256', $config['csrf_token'].$user_key.$key), 0, 20);
}
protected static function checkCSRF(string $key): void {
if (self::getCSRF($key) != ($_REQUEST['token'] ?? ''))
self::forbidden('invalid token');
}
public static function httpError(HTTPCode $http_code, string $message = ''): void {
if (self::isXhrRequest()) {
$data = [];
if ($message != '')
$data['message'] = $message;
self::ajaxError((object)$data, $http_code->value);
} else {
$http_message = preg_replace('/(?<!^)([A-Z])/', ' $1', $http_code->name);
$html = skin::getInstance()->render('error.twig', [
'code' => $http_code->value,
'title' => $http_message,
'message' => $message
]);
http_response_code($http_code->value);
echo $html;
exit;
}
}
protected static function redirect(string $url, HTTPCode $code = HTTPCode::MovedPermanently): never {
if (!in_array($code, [HTTPCode::MovedPermanently, HTTPCode::Found]))
self::internalServerError('invalid http code');
if (self::isXhrRequest())
self::ajaxOk(['redirect' => $url]);
http_response_code($code->value);
header('Location: '.$url);
exit;
}
protected static function invalidRequest(string $message = '') { self::httpError(HTTPCode::InvalidRequest, $message); }
protected static function internalServerError(string $message = '') { self::httpError(HTTPCode::InternalServerError, $message); }
protected static function notFound(string $message = '') { self::httpError(HTTPCode::NotFound, $message); }
protected static function forbidden(string $message = '') { self::httpError(HTTPCode::Forbidden, $message); }
protected static function isXhrRequest(): bool { return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'; }
protected static function ajaxOk(mixed $data): void { self::ajaxResponse(['response' => $data]); }
protected static function ajaxError(mixed $error, int $code = 200): void { self::ajaxResponse(['error' => $error], $code); }
protected static function ajaxResponse(mixed $data, int $code = 200): never {
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Content-Type: application/json; charset=utf-8');
http_response_code($code);
echo jsonEncode($data);
exit;
}
}