Router Update + Bugfix

This commit is contained in:
2022-06-01 09:47:31 +02:00
parent 658157167e
commit 1fb875fb2c
22 changed files with 589 additions and 679 deletions

View File

@@ -94,7 +94,7 @@ namespace Api\Mail {
public function _execute(): bool {
$mailConfig = $this->getMailConfig();
if (!$this->success) {
if (!$this->success || $mailConfig === null) {
return false;
}
@@ -411,7 +411,7 @@ namespace Api\Mail {
}
$mailConfig = $this->getMailConfig();
if (!$this->success) {
if (!$this->success || $mailConfig === null) {
return false;
}

View File

@@ -68,7 +68,10 @@ namespace Api\Routes {
use Api\RoutesAPI;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondBool;
use Objects\Router;
use Objects\Router\DocumentRoute;
use Objects\Router\RedirectRoute;
use Objects\Router\Router;
use Objects\Router\StaticFileRoute;
use Objects\User;
class Fetch extends RoutesAPI {
@@ -367,17 +370,17 @@ namespace Api\Routes {
$exact = $sql->parseBool($row["exact"]);
switch ($row["action"]) {
case "redirect_temporary":
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 307));
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 307));
break;
case "redirect_permanently":
$this->router->addRoute(new Router\RedirectRoute($request, $exact, $target, 308));
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 308));
break;
case "static":
$this->router->addRoute(new Router\StaticFileRoute($request, $exact, $target));
$this->router->addRoute(new StaticFileRoute($request, $exact, $target));
break;
case "dynamic":
$extra = json_decode($row["extra"]) ?? [];
$this->router->addRoute(new Router\DocumentRoute($request, $exact, $target, ...$extra));
$this->router->addRoute(new DocumentRoute($request, $exact, $target, ...$extra));
break;
default:
break;

View File

@@ -1076,6 +1076,10 @@ namespace Api\User {
}
$settings = $this->user->getConfiguration()->getSettings();
if (!$settings->isMailEnabled()) {
return $this->createError("The mail service is not enabled, please contact the server administration.");
}
if ($settings->isRecaptchaEnabled()) {
$captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->user);

View File

@@ -1,390 +0,0 @@
<?php
namespace Objects {
use Driver\Logger\Logger;
use Objects\Router\AbstractRoute;
class Router {
private User $user;
private Logger $logger;
protected array $routes;
protected array $statusCodeRoutes;
public function __construct(User $user) {
$this->user = $user;
$this->logger = new Logger("Router", $user->getSQL());
$this->routes = [];
$this->statusCodeRoutes = [];
}
public function run(string $url): string {
// TODO: do we want a global try cache and return status page 500 on any error?
// or do we want to have a global status page function here?
$url = strtok($url, "?");
foreach ($this->routes as $route) {
$pathParams = $route->match($url);
if ($pathParams !== false) {
return $route->call($this, $pathParams);
}
}
return $this->returnStatusCode(404);
}
public function returnStatusCode(int $code, array $params = []): string {
http_response_code($code);
$params["status_code"] = $code;
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
$route = $this->statusCodeRoutes[strval($code)] ?? null;
if ($route) {
return $route->call($this, $params);
} else {
$req = new \Api\Template\Render($this->user);
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
if ($res) {
return $req->getResult()["html"];
} else {
var_dump($req->getLastError());
$description = htmlspecialchars($params["status_description"]);
return "<b>$code - $description</b>";
}
}
}
public function addRoute(AbstractRoute $route) {
if (preg_match("/^\d+$/", $route->getPattern())) {
$this->statusCodeRoutes[$route->getPattern()] = $route;
} else {
$this->routes[] = $route;
}
}
public function writeCache(string $file): bool {
$routes = "";
foreach ($this->routes as $route) {
$constructor = $route->generateCache();
$routes .= "\n \$this->addRoute($constructor);";
}
$date = (new \DateTime())->format("Y/m/d H:i:s");
$code = "<?php
/**
* DO NOT EDIT!
* This file is automatically generated by the RoutesAPI on $date.
*/
namespace Cache;
use Objects\User;
use Objects\Router;
class RouterCache extends Router {
public function __construct(User \$user) {
parent::__construct(\$user);$routes
}
}
";
if (@file_put_contents($file, $code) === false) {
$this->logger->severe("Could not write Router cache file: $file");
return false;
}
return true;
}
public function getUser(): User {
return $this->user;
}
public function getLogger(): Logger {
return $this->logger;
}
public static function cleanURL(string $url, bool $cleanGET = true): string {
// strip GET parameters
if ($cleanGET) {
if (($index = strpos($url, "?")) !== false) {
$url = substr($url, 0, $index);
}
}
// strip document reference part
if (($index = strpos($url, "#")) !== false) {
$url = substr($url, 0, $index);
}
// strip leading slash
return preg_replace("/^\/+/", "", $url);
}
}
}
namespace Objects\Router {
use Api\Parameter\Parameter;
use Elements\Document;
use Objects\Router;
use PHPUnit\TextUI\ReflectionException;
abstract class AbstractRoute {
private string $pattern;
private bool $exact;
public function __construct(string $pattern, bool $exact = true) {
$this->pattern = $pattern;
$this->exact = $exact;
}
private static function parseParamType(?string $type): ?int {
if ($type === null || trim($type) === "") {
return null;
}
$type = strtolower(trim($type));
if (in_array($type, ["int", "integer"])) {
return Parameter::TYPE_INT;
} else if (in_array($type, ["float", "double"])) {
return Parameter::TYPE_FLOAT;
} else if (in_array($type, ["bool", "boolean"])) {
return Parameter::TYPE_BOOLEAN;
} else {
return Parameter::TYPE_STRING;
}
}
public function getPattern(): string {
return $this->pattern;
}
public abstract function call(Router $router, array $params): string;
protected function getArgs(): array {
return [$this->pattern, $this->exact];
}
public function generateCache(): string {
$reflection = new \ReflectionClass($this);
$className = $reflection->getName();
$args = implode(", ", array_map(function ($arg) {
return var_export($arg, true);
}, $this->getArgs()));
return "new \\$className($args)";
}
public function match(string $url) {
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
$countPattern = count($patternParts);
$patternOffset = 0;
# /test/param/optional/123
$urlParts = explode("/", $url);
$countUrl = count($urlParts);
$urlOffset = 0;
$params = [];
for (; $patternOffset < $countPattern; $patternOffset++) {
if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) {
// not a parameter? check if it matches
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
return false;
}
$urlOffset++;
} else {
// we got a parameter here
$paramDefinition = explode(":", substr($patternParts[$patternOffset], 1, -1));
$paramName = array_shift($paramDefinition);
$paramType = array_shift($paramDefinition);
$paramOptional = endsWith($paramType, "?");
if ($paramOptional) {
$paramType = substr($paramType, 0, -1);
}
$paramType = self::parseParamType($paramType);
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
if ($paramOptional) {
$param = $urlParts[$urlOffset] ?? null;
if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) {
return false;
}
$params[$paramName] = $param;
if ($urlOffset < $countUrl) {
$urlOffset++;
}
} else {
return false;
}
} else {
$param = $urlParts[$urlOffset];
if ($paramType !== null && Parameter::parseType($param) !== $paramType) {
return false;
}
$params[$paramName] = $param;
$urlOffset++;
}
}
}
if ($urlOffset !== $countUrl && $this->exact) {
return false;
}
return $params;
}
}
class EmptyRoute extends AbstractRoute {
public function __construct(string $pattern, bool $exact = true) {
parent::__construct($pattern, $exact);
}
public function call(Router $router, array $params): string {
return "";
}
}
class StaticFileRoute extends AbstractRoute {
private string $path;
private int $code;
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
parent::__construct($pattern, $exact);
$this->path = $path;
$this->code = $code;
}
public function call(Router $router, array $params): string {
http_response_code($this->code);
return serveStatic(WEBROOT, $this->path);
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->path, $this->code]);
}
}
class StaticRoute extends AbstractRoute {
private string $data;
private int $code;
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
parent::__construct($pattern, $exact);
$this->data = $data;
$this->code = $code;
}
public function call(Router $router, array $params): string {
http_response_code($this->code);
return $this->data;
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->data, $this->code]);
}
}
class RedirectRoute extends AbstractRoute {
private string $destination;
private int $code;
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
parent::__construct($pattern, $exact);
$this->destination = $destination;
$this->code = $code;
}
public function call(Router $router, array $params): string {
header("Location: $this->destination");
http_response_code($this->code);
return "";
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
}
}
class DocumentRoute extends AbstractRoute {
private string $className;
private array $args;
private ?\ReflectionClass $reflectionClass;
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
parent::__construct($pattern, $exact);
$this->className = $className;
$this->args = $args;
$this->reflectionClass = null;
}
private function loadClass(): bool {
if ($this->reflectionClass === null) {
try {
$file = getClassPath($this->className);
if (file_exists($file)) {
$this->reflectionClass = new \ReflectionClass($this->className);
if ($this->reflectionClass->isSubclassOf(Document::class)) {
return true;
}
}
} catch (ReflectionException $exception) {
$this->reflectionClass = null;
return false;
}
$this->reflectionClass = null;
return false;
}
return true;
}
public function match(string $url) {
$match = parent::match($url);
if ($match === false || !$this->loadClass()) {
return false;
}
return $match;
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->className], $this->args);
}
public function call(Router $router, array $params): string {
if (!$this->loadClass()) {
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
}
try {
$args = array_merge([$router->getUser()], $this->args);
$document = $this->reflectionClass->newInstanceArgs($args);
return $document->getCode($params);
} catch (\ReflectionException $e) {
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
}
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Objects\Router;
use Api\Parameter\Parameter;
abstract class AbstractRoute {
private string $pattern;
private bool $exact;
public function __construct(string $pattern, bool $exact = true) {
$this->pattern = $pattern;
$this->exact = $exact;
}
private static function parseParamType(?string $type): ?int {
if ($type === null || trim($type) === "") {
return null;
}
$type = strtolower(trim($type));
if (in_array($type, ["int", "integer"])) {
return Parameter::TYPE_INT;
} else if (in_array($type, ["float", "double"])) {
return Parameter::TYPE_FLOAT;
} else if (in_array($type, ["bool", "boolean"])) {
return Parameter::TYPE_BOOLEAN;
} else {
return Parameter::TYPE_STRING;
}
}
public function getPattern(): string {
return $this->pattern;
}
public abstract function call(Router $router, array $params): string;
protected function getArgs(): array {
return [$this->pattern, $this->exact];
}
public function generateCache(): string {
$reflection = new \ReflectionClass($this);
$className = $reflection->getName();
$args = implode(", ", array_map(function ($arg) {
return var_export($arg, true);
}, $this->getArgs()));
return "new \\$className($args)";
}
public function match(string $url) {
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
$countPattern = count($patternParts);
$patternOffset = 0;
# /test/param/optional/123
$urlParts = explode("/", $url);
$countUrl = count($urlParts);
$urlOffset = 0;
$params = [];
for (; $patternOffset < $countPattern; $patternOffset++) {
if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) {
// not a parameter? check if it matches
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
return false;
}
$urlOffset++;
} else {
// we got a parameter here
$paramDefinition = explode(":", substr($patternParts[$patternOffset], 1, -1));
$paramName = array_shift($paramDefinition);
$paramType = array_shift($paramDefinition);
$paramOptional = endsWith($paramType, "?");
if ($paramOptional) {
$paramType = substr($paramType, 0, -1);
}
$paramType = self::parseParamType($paramType);
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
if ($paramOptional) {
$param = $urlParts[$urlOffset] ?? null;
if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) {
return false;
}
$params[$paramName] = $param;
if ($urlOffset < $countUrl) {
$urlOffset++;
}
} else {
return false;
}
} else {
$param = $urlParts[$urlOffset];
if ($paramType !== null && Parameter::parseType($param) !== $paramType) {
return false;
}
$params[$paramName] = $param;
$urlOffset++;
}
}
}
if ($urlOffset !== $countUrl && $this->exact) {
return false;
}
return $params;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Objects\Router;
use Api\Request;
use ReflectionClass;
use ReflectionException;
class ApiRoute extends AbstractRoute {
public function __construct() {
parent::__construct("/api/{endpoint:?}/{method:?}", false);
}
public function call(Router $router, array $params): string {
$user = $router->getUser();
if (empty($params["endpoint"])) {
header("Content-Type: text/html");
$document = new \Elements\TemplateDocument($user, "swagger.twig");
return $document->getCode();
} else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $params["endpoint"])) {
http_response_code(400);
$response = createError("Invalid Method");
} else {
$apiEndpoint = ucfirst($params["endpoint"]);
if (!empty($params["method"])) {
$apiMethod = ucfirst($params["method"]);
$parentClass = "\\Api\\${apiEndpoint}API";
$apiClass = "\\Api\\${apiEndpoint}\\${apiMethod}";
} else {
$apiClass = "\\Api\\${apiEndpoint}";
$parentClass = $apiClass;
}
try {
$file = getClassPath($parentClass);
if (!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
http_response_code(404);
$response = createError("Not found");
} else {
$apiClass = new ReflectionClass($apiClass);
if(!$apiClass->isSubclassOf(Request::class) || !$apiClass->isInstantiable()) {
http_response_code(400);
$response = createError("Invalid Method");
} else {
$request = $apiClass->newInstanceArgs(array($user, true));
$request->execute();
$response = $request->getJsonResult();
}
}
} catch (ReflectionException $e) {
http_response_code(500);
$response = createError("Error instantiating class: $e");
}
}
header("Content-Type: application/json");
return $response;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Objects\Router;
use Elements\Document;
use ReflectionException;
class DocumentRoute extends AbstractRoute {
private string $className;
private array $args;
private ?\ReflectionClass $reflectionClass;
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
parent::__construct($pattern, $exact);
$this->className = $className;
$this->args = $args;
$this->reflectionClass = null;
}
private function loadClass(): bool {
if ($this->reflectionClass === null) {
try {
$file = getClassPath($this->className);
if (file_exists($file)) {
$this->reflectionClass = new \ReflectionClass($this->className);
if ($this->reflectionClass->isSubclassOf(Document::class)) {
return true;
}
}
} catch (ReflectionException $exception) {
$this->reflectionClass = null;
return false;
}
$this->reflectionClass = null;
return false;
}
return true;
}
public function match(string $url) {
$match = parent::match($url);
if ($match === false || !$this->loadClass()) {
return false;
}
return $match;
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->className], $this->args);
}
public function call(Router $router, array $params): string {
if (!$this->loadClass()) {
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
}
try {
$args = array_merge([$router->getUser()], $this->args);
$document = $this->reflectionClass->newInstanceArgs($args);
return $document->getCode($params);
} catch (\ReflectionException $e) {
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Objects\Router;
class EmptyRoute extends AbstractRoute {
public function __construct(string $pattern, bool $exact = true) {
parent::__construct($pattern, $exact);
}
public function call(Router $router, array $params): string {
return "";
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Objects\Router;
class RedirectRoute extends AbstractRoute {
private string $destination;
private int $code;
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
parent::__construct($pattern, $exact);
$this->destination = $destination;
$this->code = $code;
}
public function call(Router $router, array $params): string {
header("Location: $this->destination");
http_response_code($this->code);
return "";
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Objects\Router;
use Driver\Logger\Logger;
use Objects\User;
class Router {
private User $user;
private Logger $logger;
protected array $routes;
protected array $statusCodeRoutes;
public function __construct(User $user) {
$this->user = $user;
$this->logger = new Logger("Router", $user->getSQL());
$this->routes = [];
$this->statusCodeRoutes = [];
$this->addRoute(new ApiRoute());
}
public function run(string $url): string {
// TODO: do we want a global try cache and return status page 500 on any error?
// or do we want to have a global status page function here?
$url = strtok($url, "?");
foreach ($this->routes as $route) {
$pathParams = $route->match($url);
if ($pathParams !== false) {
return $route->call($this, $pathParams);
}
}
return $this->returnStatusCode(404);
}
public function returnStatusCode(int $code, array $params = []): string {
http_response_code($code);
$params["status_code"] = $code;
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
$route = $this->statusCodeRoutes[strval($code)] ?? null;
if ($route) {
return $route->call($this, $params);
} else {
$req = new \Api\Template\Render($this->user);
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
if ($res) {
return $req->getResult()["html"];
} else {
var_dump($req->getLastError());
$description = htmlspecialchars($params["status_description"]);
return "<b>$code - $description</b>";
}
}
}
public function addRoute(AbstractRoute $route) {
if (preg_match("/^\d+$/", $route->getPattern())) {
$this->statusCodeRoutes[$route->getPattern()] = $route;
} else {
$this->routes[] = $route;
}
}
public function writeCache(string $file): bool {
$routes = "";
foreach ($this->routes as $route) {
$constructor = $route->generateCache();
$routes .= "\n \$this->addRoute($constructor);";
}
$date = (new \DateTime())->format("Y/m/d H:i:s");
$code = "<?php
/**
* DO NOT EDIT!
* This file is automatically generated by the RoutesAPI on $date.
*/
namespace Cache;
use Objects\User;
use Objects\Router\Router;
class RouterCache extends Router {
public function __construct(User \$user) {
parent::__construct(\$user);$routes
}
}
";
if (@file_put_contents($file, $code) === false) {
$this->logger->severe("Could not write Router cache file: $file");
return false;
}
return true;
}
public function getUser(): User {
return $this->user;
}
public function getLogger(): Logger {
return $this->logger;
}
public static function cleanURL(string $url, bool $cleanGET = true): string {
// strip GET parameters
if ($cleanGET) {
if (($index = strpos($url, "?")) !== false) {
$url = substr($url, 0, $index);
}
}
// strip document reference part
if (($index = strpos($url, "#")) !== false) {
$url = substr($url, 0, $index);
}
// strip leading slash
return preg_replace("/^\/+/", "", $url);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Objects\Router;
class StaticFileRoute extends AbstractRoute {
private string $path;
private int $code;
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
parent::__construct($pattern, $exact);
$this->path = $path;
$this->code = $code;
}
public function call(Router $router, array $params): string {
http_response_code($this->code);
return serveStatic(WEBROOT, $this->path);
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->path, $this->code]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Objects\Router;
class StaticRoute extends AbstractRoute {
private string $data;
private int $code;
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
parent::__construct($pattern, $exact);
$this->data = $data;
$this->code = $code;
}
public function call(Router $router, array $params): string {
http_response_code($this->code);
return $this->data;
}
protected function getArgs(): array {
return array_merge(parent::getArgs(), [$this->data, $this->code]);
}
}

View File

@@ -20,7 +20,10 @@
</div>
<input type="password" autocomplete='password' name='password' id='password' class="form-control" placeholder="Password">
</div>
<div class="input-group mt-5 mb-4">
<div class="ml-2" style="line-height: 38px;">
<a href="/resetPassword">Forgot Password?</a>
</div>
<div class="input-group mt-3 mb-4">
<button type="button" class="btn btn-primary" id='btnLogin'>Sign In</button>
{% if site.registrationEnabled %}
<div class="ml-2" style="line-height: 38px;">Don't have an account yet? <a href="/register">Click here</a> to register.</div>

View File

@@ -25,6 +25,7 @@
You can either <a href="javascript:history.back()">Go Back to previous page</a>
or try to <a href="javascript:document.location.reload()">reload the page</a>.
</p>
<p>{{ message }}</p>
</div>
</div>
</div>