239 lines
8.7 KiB
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;
|
|
}
|
|
|
|
}
|