4in1_ws_web/src/engine/http/RequestHandler.php

209 lines
7.7 KiB
PHP

<?php
namespace engine\http;
use engine\exceptions\InvalidDomainException;
use engine\exceptions\NotImplementedException;
use engine\http\errors\BaseRedirect;
use engine\http\errors\HTTPError;
use engine\http\errors\InvalidRequest;
use engine\http\errors\NotFound;
use engine\http\errors\NotImplemented;
use engine\Router;
use engine\skin\BaseSkin;
use engine\skin\ErrorSkin;
abstract class RequestHandler
{
protected array $routerInput = [];
public readonly BaseSkin $skin;
public static final function resolveAndDispatch(): void {
global $config, $globalContext;
try {
if ($_SERVER['HTTP_HOST'] !== $config['domain'] && !str_ends_with($_SERVER['HTTP_HOST'], '.'.$config['domain']))
throw new InvalidDomainException('invalid domain '.$_SERVER['HTTP_HOST']);
if (strlen($_SERVER['HTTP_HOST']) > ($orig_domain_len = strlen($config['domain']))) {
$sub = substr($_SERVER['HTTP_HOST'], 0, -$orig_domain_len - 1);
if (!array_key_exists($sub, $config['subdomains']))
throw new InvalidDomainException('invalid subdomain '.$sub);
$globalContext->setProject($config['subdomains'][$sub]);
} else {
$globalContext->setProject($config['project']);
}
if (!in_array($_SERVER['REQUEST_METHOD'], ['POST', 'GET']))
throw new 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)
throw new NotFound('Route not found');
$route = preg_split('/ +/', $route);
$handler_class = 'app\\'.$globalContext->project.'\\'.$route[0].'Handler';
if (!class_exists($handler_class))
throw new 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);
}
}
$rh = new $handler_class();
$globalContext->setRequestHandler($rh);
$response = $rh->callAct($_SERVER['REQUEST_METHOD'], $action, $input);
}
catch (InvalidDomainException|NotImplementedException $e) {
$stacktrace = \engine\logging\Util::getErrorFullStackTrace($e);
logError($e, stacktrace: $stacktrace);
self::renderError($e->getMessage(), HTTPCode::InternalServerError, $stacktrace);
}
catch (BaseRedirect $e) {
if (isXHRRequest()) {
$response = new AjaxOk(['redirect' => $e->getLocation()]);
} else {
header('Location: '.$e->getLocation(), $e->shouldReplace(), $e->getHTTPCode()->value);
exit;
}
}
catch (HTTPError $e) {
if (isXHRRequest()) {
$data = [];
$message = $e->getDescription();
if ($message != '')
$data['message'] = $message;
$response = new AjaxError((object)$data, $e->getHTTPCode());
} else {
self::renderError($e->getMessage(), $e->getHTTPCode());
}
}
catch (\Throwable $e) {
$stacktrace = \engine\logging\Util::getErrorFullStackTrace($e);
logError(get_class($e).': '.$e->getMessage(), stacktrace: $stacktrace);
self::renderError(get_class($e).': '.$e->getMessage(), HTTPCode::InternalServerError, $stacktrace);
}
finally {
if (!empty($response)) {
if ($response instanceof Response) {
$response->send();
} else {
logError(__METHOD__.': $response is not Response');
}
}
}
}
protected static function renderError(string $message, HTTPCode $code, ?string $stacktrace = null): never {
http_response_code($code->value);
$skin = new ErrorSkin();
switch ($code) {
case HTTPCode::NotFound:
$skin->renderNotFound();
default:
$show_debug_info = isDev() || isAdmin();
$skin->renderError($code->getTitle(),
$show_debug_info ? $message : null,
$show_debug_info ? $stacktrace : null);
}
}
protected 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))
throw new NotFound(static::class.'::'.$handler_method.' is not defined');
if (!(new \ReflectionMethod($this, $handler_method)->isPublic()))
throw new 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] : []);
}
protected 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];
}
public function input(string $input,
bool $trim = false): array
{
$input = preg_split('/,\s+?/', $input, -1, PREG_SPLIT_NO_EMPTY);
$result = [];
foreach ($input as $var) {
$pos = strpos($var, ':');
if ($pos === 1) {
$type = InputVarType::from(substr($var, 0, $pos));
$name = trim(substr($var, $pos + 1));
} else {
$type = 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 = implode($val);
$result[] = match ($type) {
InputVarType::INTEGER => (int)$val,
InputVarType::FLOAT => (float)$val,
InputVarType::BOOLEAN => (bool)$val,
default => $trim ? trim((string)$val) : (string)$val
};
}
return $result;
}
public function getCSRF(string $key): string {
global $config;
$user_key = isAdmin() ? \app\Admin::getCSRFSalt() : $_SERVER['REMOTE_ADDR'];
return substr(hash('sha256', $config['csrf_token'].$user_key.$key), 0, 20);
}
/**
* @throws errors\Forbidden
*/
protected function checkCSRF(string $key): void {
if ($this->getCSRF($key) != ($_REQUEST['token'] ?? ''))
throw new errors\Forbidden('invalid token');
}
/**
* @throws InvalidRequest
*/
protected function ensureIsXHR(): void {
if (!isXHRRequest())
throw new InvalidRequest();
}
}