209 lines
7.7 KiB
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();
|
|
}
|
|
}
|