mirror of
https://github.com/openvk/chandler.git
synced 2025-03-31 21:43:59 +03:00
Reformatting the Router class.
This commit is contained in:
parent
c89cd500a1
commit
5d7b131240
1 changed files with 165 additions and 197 deletions
|
@ -1,126 +1,106 @@
|
|||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Chandler\MVC\Routing;
|
||||
use Chandler\Patterns\TSimpleSingleton;
|
||||
|
||||
use Chandler\Eventing\EventDispatcher;
|
||||
use Chandler\Session\Session;
|
||||
use Chandler\MVC\Exceptions\InterruptedException;
|
||||
use Chandler\MVC\IPresenter;
|
||||
use Chandler\Patterns\TSimpleSingleton;
|
||||
use Chandler\Session\Session;
|
||||
use Nette\DI;
|
||||
use Nette\DI\Config\Adapters\NeonAdapter;
|
||||
use Nette\DI\Config\Loader;
|
||||
use SodiumException;
|
||||
|
||||
class Router
|
||||
{
|
||||
const HANDLER_DELIMITER = "%([#@❤]|\->)%";
|
||||
const ALIAS_REGEX = "%{(\??\!?([A-z]++))}%";
|
||||
const ALIAS_REGEX = "%{(\??\!?([A-z]++))}%";
|
||||
|
||||
private $url = NULL;
|
||||
private $routes = [];
|
||||
private $statics = [];
|
||||
private $scope = [];
|
||||
const HANDLER_DELIMITER = "%([#@❤]|\->)%";
|
||||
|
||||
private $events;
|
||||
|
||||
private $routes = [];
|
||||
|
||||
private $scope = [];
|
||||
|
||||
private $statics = [];
|
||||
|
||||
private $url = null;
|
||||
|
||||
protected $types = [
|
||||
"num" => "(-?\d++)",
|
||||
"num" => "(-?\d++)",
|
||||
"text" => "([A-z0-9]++)",
|
||||
"slug" => "([A-z0-9А-я\-_ ]++)",
|
||||
];
|
||||
|
||||
private function __construct()
|
||||
private function computeRegExp(string $route, array $customAliases = [], ?string $prefix = null): string
|
||||
{
|
||||
$this->events = EventDispatcher::i();
|
||||
}
|
||||
|
||||
private function computeRegExp(string $route, array $customAliases = [], ?string $prefix = NULL): string
|
||||
{
|
||||
$regexp = preg_replace_callback(Router::ALIAS_REGEX, function($matches) use ($customAliases) {
|
||||
if($matches[1][0] === "?") {
|
||||
$regexp = preg_replace_callback(Router::ALIAS_REGEX, function ($matches) use ($customAliases) {
|
||||
if ($matches[1][0] === "?") {
|
||||
$replacement = !isset($customAliases[$matches[2]])
|
||||
? NULL
|
||||
: ($matches[1][1] !== "!" ? "(" : "(?:") . $customAliases[$matches[2]] . ")";
|
||||
? null
|
||||
: ($matches[1][1] !== "!" ? "(" : "(?:") . $customAliases[$matches[2]] . ")";
|
||||
} else {
|
||||
$replacement = $this->types[$matches[1]];
|
||||
}
|
||||
|
||||
if(!$replacement) {
|
||||
$exMessage = "Unknown type alias: $matches[1].";
|
||||
if (!$replacement) {
|
||||
$exMessage = "Unknown type alias: $matches[1].";
|
||||
$exMessage .= " (Available options are: " . implode(", ", array_keys($this->types));
|
||||
if(sizeof($customAliases) > 0)
|
||||
if (sizeof($customAliases) > 0)
|
||||
$exMessage .= " or any of these user-defined aliases: " . implode(", ", array_keys($customAliases)) . ")";
|
||||
else
|
||||
$exMessage .= ")";
|
||||
|
||||
throw new Exceptions\UnknownTypeAliasException($exMessage);
|
||||
}
|
||||
|
||||
return $replacement;
|
||||
}, addslashes($route));
|
||||
|
||||
if(!is_null($prefix)) {
|
||||
if (!is_null($prefix)) {
|
||||
$regexp = "\\/$prefix\\" . ($route === "/" ? "/" : "/$regexp");
|
||||
}
|
||||
|
||||
return "%^$regexp$%";
|
||||
}
|
||||
|
||||
function makeCSRFToken(Route $route, string $nonce): string
|
||||
private function delegateController(string $namespace, string $presenterName, string $action, array $parameters = []): string
|
||||
{
|
||||
$key = hash("snefru", CHANDLER_ROOT_CONF["security"]["secret"] . bin2hex($nonce));
|
||||
|
||||
$data = $route->namespace;
|
||||
$data .= Session::i()->get("tok", -1);
|
||||
|
||||
return hash_hmac("snefru", $data, $key) . "#" . bin2hex($nonce);
|
||||
}
|
||||
|
||||
private function setCSRFStatus(Route $route): void
|
||||
{
|
||||
if(CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "disabled") {
|
||||
$GLOBALS["csrfCheck"] = true;
|
||||
} else {
|
||||
$GLOBALS["csrfCheck"] = false;
|
||||
|
||||
$hash = ($_GET["hash"] ?? ($_POST["hash"] ?? false));
|
||||
if($hash !== false) {
|
||||
$data = explode("#", $hash);
|
||||
|
||||
try {
|
||||
if(!isset($data[0]) || !isset($data[1])) throw new \SodiumException;
|
||||
[$hash, $nonce] = $data;
|
||||
|
||||
if(sodium_memcmp($this->makeCSRFToken($route, hex2bin($nonce)), "$hash#$nonce") === 0) {
|
||||
if(CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "permissive")
|
||||
$GLOBALS["csrfCheck"] = true;
|
||||
else if(CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "strict")
|
||||
$GLOBALS["csrfCheck"] = parse_url($_SERVER["HTTP_REFERER"], PHP_URL_HOST) === $_SERVER["HTTP_HOST"];
|
||||
else
|
||||
trigger_error("Bad value for chandler.security.csrfProtection: disabled, permissive or strict expected.", E_USER_ERROR);
|
||||
}
|
||||
} catch(\SodiumException $ex) {}
|
||||
$presenter = $this->getPresenter($namespace, $presenterName);
|
||||
$action = ucfirst($action);
|
||||
try {
|
||||
$presenter->onStartup();
|
||||
$presenter->{"render$action"}(...$parameters);
|
||||
$presenter->onBeforeRender();
|
||||
$this->scope += array_merge_recursive($presenter->getTemplateScope(), []); #TODO: add default parameters
|
||||
#TODO: move this to delegateView
|
||||
$tpl = $this->scope["_template"] ?? "$presenterName/$action.xml";
|
||||
if ($tpl[0] !== "/") {
|
||||
$dir = CHANDLER_EXTENSIONS_ENABLED . "/$namespace/Web/Presenters/templates";
|
||||
$tpl = "$dir/$tpl";
|
||||
if (isset($this->scope["_templatePath"]))
|
||||
$tpl = str_replace($dir, $this->scope["_templatePath"], $tpl);
|
||||
}
|
||||
if (!file_exists($tpl)) {
|
||||
trigger_error("Could not open $tpl as template, falling back.", E_USER_NOTICE);
|
||||
$tpl = "$presenterName/$action.xml";
|
||||
}
|
||||
$output = $this->delegateView($tpl, $presenter);
|
||||
$presenter->onAfterRender();
|
||||
} catch (InterruptedException $ex) {
|
||||
}
|
||||
|
||||
$GLOBALS["csrfToken"] = $this->makeCSRFToken($route, openssl_random_pseudo_bytes(4));
|
||||
$presenter->onStop();
|
||||
$presenter->onDestruction();
|
||||
$presenter = null;
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getDI(string $namespace): DI\Container
|
||||
private function delegateRoute(Route $route, array $matches): string
|
||||
{
|
||||
$loader = new DI\ContainerLoader(CHANDLER_ROOT . "/tmp/cache/di_$namespace", true);
|
||||
$class = $loader->load(function($compiler) use ($namespace) {
|
||||
$fileLoader = new \Nette\DI\Config\Loader;
|
||||
$fileLoader->addAdapter("yml", \Nette\DI\Config\Adapters\NeonAdapter::class);
|
||||
|
||||
$compiler->loadConfig(CHANDLER_EXTENSIONS_ENABLED . "/$namespace/Web/di.yml", $fileLoader);
|
||||
});
|
||||
|
||||
return new $class;
|
||||
}
|
||||
|
||||
private function getPresenter(string $namespace, string $presenterName): ?IPresenter
|
||||
{
|
||||
$di = $this->getDI($namespace);
|
||||
|
||||
$services = $di->findByType("\\$namespace\\Web\\Presenters\\$presenterName" . "Presenter", false);
|
||||
return $di->getService($services[0], false);
|
||||
$parameters = [];
|
||||
foreach ($matches as $param)
|
||||
$parameters[] = is_numeric($param) ? (int)$param : $param;
|
||||
$this->setCSRFStatus($route);
|
||||
return $this->delegateController($route->namespace, $route->presenter, $route->action, $parameters);
|
||||
}
|
||||
|
||||
private function delegateView(string $filename, IPresenter $presenter): string
|
||||
|
@ -128,115 +108,112 @@ class Router
|
|||
return $presenter->getTemplatingEngine()->renderToString($filename, $this->scope);
|
||||
}
|
||||
|
||||
private function delegateController(string $namespace, string $presenterName, string $action, array $parameters = []): string
|
||||
private function getDI(string $namespace): DI\Container
|
||||
{
|
||||
$presenter = $this->getPresenter($namespace, $presenterName);
|
||||
$action = ucfirst($action);
|
||||
|
||||
try {
|
||||
$presenter->onStartup();
|
||||
$presenter->{"render$action"}(...$parameters);
|
||||
$presenter->onBeforeRender();
|
||||
|
||||
$this->scope += array_merge_recursive($presenter->getTemplateScope(), []); #TODO: add default parameters
|
||||
#TODO: move this to delegateView
|
||||
|
||||
$tpl = $this->scope["_template"] ?? "$presenterName/$action.xml";
|
||||
if($tpl[0] !== "/") {
|
||||
$dir = CHANDLER_EXTENSIONS_ENABLED . "/$namespace/Web/Presenters/templates";
|
||||
$tpl = "$dir/$tpl";
|
||||
if(isset($this->scope["_templatePath"]))
|
||||
$tpl = str_replace($dir, $this->scope["_templatePath"], $tpl);
|
||||
}
|
||||
|
||||
if(!file_exists($tpl)) {
|
||||
trigger_error("Could not open $tpl as template, falling back.", E_USER_NOTICE);
|
||||
$tpl = "$presenterName/$action.xml";
|
||||
}
|
||||
|
||||
$output = $this->delegateView($tpl, $presenter);
|
||||
|
||||
$presenter->onAfterRender();
|
||||
} catch(InterruptedException $ex) {}
|
||||
|
||||
$presenter->onStop();
|
||||
$presenter->onDestruction();
|
||||
$presenter = NULL;
|
||||
|
||||
return $output;
|
||||
$loader = new DI\ContainerLoader(CHANDLER_ROOT . "/tmp/cache/di_$namespace", true);
|
||||
$class = $loader->load(function ($compiler) use ($namespace) {
|
||||
$fileLoader = new Loader;
|
||||
$fileLoader->addAdapter("yml", NeonAdapter::class);
|
||||
$compiler->loadConfig(CHANDLER_EXTENSIONS_ENABLED . "/$namespace/Web/di.yml", $fileLoader);
|
||||
});
|
||||
return new $class;
|
||||
}
|
||||
|
||||
private function delegateRoute(Route $route, array $matches): string
|
||||
private function getPresenter(string $namespace, string $presenterName): ?IPresenter
|
||||
{
|
||||
$parameters = [];
|
||||
$di = $this->getDI($namespace);
|
||||
$services = $di->findByType("\\$namespace\\Web\\Presenters\\$presenterName" . "Presenter", false);
|
||||
return $di->getService($services[0], false);
|
||||
}
|
||||
|
||||
foreach($matches as $param)
|
||||
$parameters[] = is_numeric($param) ? (int) $param : $param;
|
||||
|
||||
$this->setCSRFStatus($route);
|
||||
return $this->delegateController($route->namespace, $route->presenter, $route->action, $parameters);
|
||||
private function setCSRFStatus(Route $route): void
|
||||
{
|
||||
if (CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "disabled") {
|
||||
$GLOBALS["csrfCheck"] = true;
|
||||
} else {
|
||||
$GLOBALS["csrfCheck"] = false;
|
||||
$hash = ($_GET["hash"] ?? ($_POST["hash"] ?? false));
|
||||
if ($hash !== false) {
|
||||
$data = explode("#", $hash);
|
||||
try {
|
||||
if (!isset($data[0]) || !isset($data[1])) throw new SodiumException;
|
||||
[$hash, $nonce] = $data;
|
||||
if (sodium_memcmp($this->makeCSRFToken($route, hex2bin($nonce)), "$hash#$nonce") === 0) {
|
||||
if (CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "permissive")
|
||||
$GLOBALS["csrfCheck"] = true;
|
||||
else if (CHANDLER_ROOT_CONF["security"]["csrfProtection"] === "strict")
|
||||
$GLOBALS["csrfCheck"] = parse_url($_SERVER["HTTP_REFERER"], PHP_URL_HOST) === $_SERVER["HTTP_HOST"];
|
||||
else
|
||||
trigger_error("Bad value for chandler.security.csrfProtection: disabled, permissive or strict expected.", E_USER_ERROR);
|
||||
}
|
||||
} catch (SodiumException $ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
$GLOBALS["csrfToken"] = $this->makeCSRFToken($route, openssl_random_pseudo_bytes(4));
|
||||
}
|
||||
|
||||
function delegateStatic(string $namespace, string $path): string
|
||||
{
|
||||
$static = $static = $this->statics[$namespace];
|
||||
if(!isset($static)) return "Fatal error: no route";
|
||||
|
||||
if(!file_exists($file = "$static/$path"))
|
||||
if (!isset($static)) return "Fatal error: no route";
|
||||
if (!file_exists($file = "$static/$path"))
|
||||
return "Fatal error: no resource";
|
||||
|
||||
$hash = "W/\"" . hash_file("snefru", $file) . "\"";
|
||||
if(isset($_SERVER["HTTP_IF_NONE_MATCH"]))
|
||||
if($_SERVER["HTTP_IF_NONE_MATCH"] === $hash)
|
||||
if (isset($_SERVER["HTTP_IF_NONE_MATCH"]))
|
||||
if ($_SERVER["HTTP_IF_NONE_MATCH"] === $hash)
|
||||
exit(header("HTTP/1.1 304"));
|
||||
|
||||
header("Content-Type: " . system_extension_mime_type($file) ?? "text/plain; charset=unknown-8bit");
|
||||
header("Content-Size: " . filesize($file));
|
||||
header("ETag: $hash");
|
||||
|
||||
readfile($file);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
function reverse(string $hotlink, ...$parameters): ?string
|
||||
function execute(string $url, ?string $parentModule = null): ?string
|
||||
{
|
||||
if(sizeof($j = explode("!", $hotlink)) === 2)
|
||||
[$namespace, $hotlink] = $j;
|
||||
else
|
||||
$namespace = explode("\\", $this->scope["parentModule"])[0];
|
||||
|
||||
[$presenter, $action] = preg_split(Router::HANDLER_DELIMITER, $hotlink);
|
||||
|
||||
foreach($this->routes as $route) {
|
||||
if($route->namespace !== $namespace || $route->presenter !== $presenter) continue;
|
||||
if(!is_null($action) && $route->action != $action) continue;
|
||||
|
||||
$count = preg_match_all(Router::ALIAS_REGEX, $route->raw);
|
||||
if($count != sizeof($parameters)) continue;
|
||||
|
||||
$i = 0;
|
||||
return preg_replace_callback(Router::ALIAS_REGEX, function() use ($parameters, &$i) {
|
||||
return $parameters[$i++];
|
||||
}, $route->raw);
|
||||
$this->url = chandler_escape_url(parse_url($url, PHP_URL_PATH));
|
||||
if (!is_null($parentModule)) {
|
||||
$GLOBALS["parentModule"] = $parentModule;
|
||||
$this->scope["parentModule"] = $GLOBALS["parentModule"];
|
||||
}
|
||||
if (preg_match("%^\/assets\/packages\/static\/([A-z_\\-]++)\/(.++)$%", $this->url, $matches)) {
|
||||
[$j, $namespace, $file] = $matches;
|
||||
return $this->delegateStatic($namespace, $file);
|
||||
}
|
||||
$match = $this->getMatchingRoute($this->url);
|
||||
if (!$match)
|
||||
return null;
|
||||
return $this->delegateRoute(...$match);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
function getMatchingRoute(string $url): ?array
|
||||
{
|
||||
foreach ($this->routes as $route)
|
||||
if (preg_match($route->regex, $url, $matches))
|
||||
return [$route, array_slice($matches, 1)];
|
||||
return null;
|
||||
}
|
||||
|
||||
function makeCSRFToken(Route $route, string $nonce): string
|
||||
{
|
||||
$key = hash("snefru", CHANDLER_ROOT_CONF["security"]["secret"] . bin2hex($nonce));
|
||||
$data = $route->namespace;
|
||||
$data .= Session::i()->get("tok", -1);
|
||||
return hash_hmac("snefru", $data, $key) . "#" . bin2hex($nonce);
|
||||
}
|
||||
|
||||
function push(?string $prefix, string $url, string $namespace, string $presenter, string $action, array $ph): void
|
||||
{
|
||||
$route = new Route;
|
||||
$route->raw = $url;
|
||||
if(!is_null($prefix))
|
||||
if (!is_null($prefix))
|
||||
$route->raw = "/$prefix" . $route->raw;
|
||||
|
||||
$route->regex = $this->computeRegExp($url, $ph, $prefix);
|
||||
$route->regex = $this->computeRegExp($url, $ph, $prefix);
|
||||
$route->namespace = $namespace;
|
||||
$route->presenter = $presenter;
|
||||
$route->action = $action;
|
||||
|
||||
$this->routes[] = $route;
|
||||
$route->action = $action;
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
function pushStatic(string $namespace, string $path): void
|
||||
|
@ -247,51 +224,42 @@ class Router
|
|||
function readRoutes(string $filename, string $namespace, bool $autoprefix = true): void
|
||||
{
|
||||
$config = chandler_parse_yaml($filename);
|
||||
|
||||
if(isset($config["static"]))
|
||||
if (isset($config["static"]))
|
||||
$this->pushStatic($namespace, CHANDLER_EXTENSIONS_ENABLED . "/$namespace/Web/$config[static]");
|
||||
|
||||
if(isset($config["include"]))
|
||||
foreach($config["include"] as $include)
|
||||
if (isset($config["include"]))
|
||||
foreach ($config["include"] as $include)
|
||||
$this->readRoutes(dirname($filename) . "/$include", $namespace, $autoprefix);
|
||||
|
||||
foreach($config["routes"] as $route) {
|
||||
$route = (object) $route;
|
||||
foreach ($config["routes"] as $route) {
|
||||
$route = (object)$route;
|
||||
$placeholders = $route->placeholders ?? [];
|
||||
[$presenter, $action] = preg_split(Router::HANDLER_DELIMITER, $route->handler);
|
||||
|
||||
$this->push($autoprefix ? $namespace : NULL, $route->url, $namespace, $presenter, $action, $placeholders);
|
||||
$this->push($autoprefix ? $namespace : null, $route->url, $namespace, $presenter, $action, $placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchingRoute(string $url): ?array
|
||||
function reverse(string $hotlink, ...$parameters): ?string
|
||||
{
|
||||
foreach($this->routes as $route)
|
||||
if(preg_match($route->regex, $url, $matches))
|
||||
return [$route, array_slice($matches, 1)];
|
||||
|
||||
return NULL;
|
||||
if (sizeof($j = explode("!", $hotlink)) === 2)
|
||||
[$namespace, $hotlink] = $j;
|
||||
else
|
||||
$namespace = explode("\\", $this->scope["parentModule"])[0];
|
||||
[$presenter, $action] = preg_split(Router::HANDLER_DELIMITER, $hotlink);
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route->namespace !== $namespace || $route->presenter !== $presenter) continue;
|
||||
if (!is_null($action) && $route->action != $action) continue;
|
||||
$count = preg_match_all(Router::ALIAS_REGEX, $route->raw);
|
||||
if ($count != sizeof($parameters)) continue;
|
||||
$i = 0;
|
||||
return preg_replace_callback(Router::ALIAS_REGEX, function () use ($parameters, &$i) {
|
||||
return $parameters[$i++];
|
||||
}, $route->raw);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function execute(string $url, ?string $parentModule = null): ?string
|
||||
private function __construct()
|
||||
{
|
||||
$this->url = chandler_escape_url(parse_url($url, PHP_URL_PATH));
|
||||
|
||||
if(!is_null($parentModule)) {
|
||||
$GLOBALS["parentModule"] = $parentModule;
|
||||
$this->scope["parentModule"] = $GLOBALS["parentModule"];
|
||||
}
|
||||
|
||||
if(preg_match("%^\/assets\/packages\/static\/([A-z_\\-]++)\/(.++)$%", $this->url, $matches)) {
|
||||
[$j, $namespace, $file] = $matches;
|
||||
return $this->delegateStatic($namespace, $file);
|
||||
}
|
||||
|
||||
$match = $this->getMatchingRoute($this->url);
|
||||
if(!$match)
|
||||
return NULL;
|
||||
|
||||
return $this->delegateRoute(...$match);
|
||||
$this->events = EventDispatcher::i();
|
||||
}
|
||||
|
||||
use TSimpleSingleton;
|
||||
|
|
Loading…
Reference in a new issue