diff --git a/README.md b/README.md index 88c8dd8..00c34c5 100644 --- a/README.md +++ b/README.md @@ -181,13 +181,13 @@ To create a new document, a class inside [/core/Documents](/core/Documents) is c namespace Documents { use Elements\Document; - use Objects\User; + use Objects\Router\Router; use Documents\Example\ExampleHead; use Documents\Example\ExampleBody; class ExampleDocument extends Document { - public function __construct(User $user, ?string $view = NULL) { - parent::__construct($user, ExampleHead::class, ExampleBody::class, $view); + public function __construct(Router $router, ?string $view = NULL) { + parent::__construct($router, ExampleHead::class, ExampleBody::class, $view); } } } diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 7899955..2720139 100644 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -163,6 +163,7 @@ class CreateDatabase extends DatabaseScript { ->addRow("/resetPassword", "dynamic", "\\Documents\\Account", json_encode(["account/reset_password.twig"]), true) ->addRow("/login", "dynamic", "\\Documents\\Account", json_encode(["account/login.twig"]), true) ->addRow("/resendConfirmEmail", "dynamic", "\\Documents\\Account", json_encode(["account/resend_confirm_email.twig"]), true) + ->addRow("/debug", "dynamic", "\\Documents\\Info", NULL, true) ->addRow("/", "static", "/static/welcome.html", NULL, true); $queries[] = $sql->createTable("Settings") @@ -228,7 +229,7 @@ class CreateDatabase extends DatabaseScript { ->addDateTime("nextTry", false, $sql->now()) ->addString("errorMessage", NULL, true) ->primaryKey("uid"); - $queries = array_merge($queries, \Configuration\Patch\log::createTableLog($sql, "MailQueue", 30)); + $queries = array_merge($queries, \Configuration\Patch\EntityLog_2021_04_08::createTableLog($sql, "MailQueue", 30)); $queries[] = $sql->createTable("News") ->addSerial("uid") diff --git a/core/Configuration/Patch/log.class.php b/core/Configuration/Patch/EntityLog_2021_04_08.class.php similarity index 98% rename from core/Configuration/Patch/log.class.php rename to core/Configuration/Patch/EntityLog_2021_04_08.class.php index 0f1980d..f43a480 100644 --- a/core/Configuration/Patch/log.class.php +++ b/core/Configuration/Patch/EntityLog_2021_04_08.class.php @@ -11,7 +11,7 @@ use Driver\SQL\Type\CurrentColumn; use Driver\SQL\Type\CurrentTable; use Driver\SQL\Type\Trigger; -class log extends DatabaseScript { +class EntityLog_2021_04_08 extends DatabaseScript { public static function createTableLog(SQL $sql, string $table, int $lifetime = 90): array { return [ diff --git a/core/Documents/Account.class.php b/core/Documents/Account.class.php index 75b0d2d..0bdbaad 100644 --- a/core/Documents/Account.class.php +++ b/core/Documents/Account.class.php @@ -4,12 +4,12 @@ namespace Documents; use Elements\TemplateDocument; -use Objects\User; +use Objects\Router\Router; class Account extends TemplateDocument { - public function __construct(User $user, string $templateName) { - parent::__construct($user, $templateName); + public function __construct(Router $router, string $templateName) { + parent::__construct($router, $templateName); $this->enableCSP(); } @@ -34,13 +34,13 @@ class Account extends TemplateDocument { } } } else if ($this->getTemplateName() === "account/register.twig") { - $settings = $this->user->getConfiguration()->getSettings(); - if ($this->user->isLoggedIn()) { + $settings = $this->getSettings(); + if ($this->getUser()->isLoggedIn()) { $this->createError("You are already logged in."); } else if (!$settings->isRegistrationAllowed()) { $this->createError("Registration is not enabled on this website."); } - } else if ($this->getTemplateName() === "account/login.twig" && $this->user->isLoggedIn()) { + } else if ($this->getTemplateName() === "account/login.twig" && $this->getUser()->isLoggedIn()) { header("Location: /admin"); exit(); } else if ($this->getTemplateName() === "account/accept_invite.twig") { diff --git a/core/Documents/Admin.class.php b/core/Documents/Admin.class.php index e1740d4..ac6711e 100644 --- a/core/Documents/Admin.class.php +++ b/core/Documents/Admin.class.php @@ -3,13 +3,14 @@ namespace Documents; use Elements\TemplateDocument; -use Objects\User; +use Objects\Router\Router; class Admin extends TemplateDocument { - public function __construct(User $user) { + public function __construct(Router $router) { + $user = $router->getUser(); $template = $user->isLoggedIn() ? "admin.twig" : "redirect.twig"; $params = $user->isLoggedIn() ? [] : ["url" => "/login"]; - parent::__construct($user, $template, $params); + parent::__construct($router, $template, $params); $this->enableCSP(); } } \ No newline at end of file diff --git a/core/Documents/Info.class.php b/core/Documents/Info.class.php new file mode 100644 index 0000000..369cec0 --- /dev/null +++ b/core/Documents/Info.class.php @@ -0,0 +1,26 @@ +getDocument()->getUser(); + if ($user->isLoggedIn() && $user->hasGroup(USER_GROUP_ADMIN)) { + phpinfo(); + } else { + $message = "You are not logged in or do not have the proper privileges to access this page."; + return $this->getDocument()->getRouter()->returnStatusCode(403, [ "message" => $message] ); + } + } +} \ No newline at end of file diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index 087fc54..6bd9ecd 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -5,10 +5,11 @@ namespace Documents { use Documents\Install\InstallBody; use Documents\Install\InstallHead; use Elements\HtmlDocument; + use Objects\Router\Router; class Install extends HtmlDocument { - public function __construct($user) { - parent::__construct($user, InstallHead::class, InstallBody::class); + public function __construct(Router $router) { + parent::__construct($router, InstallHead::class, InstallBody::class); $this->databaseRequired = false; } } diff --git a/core/Elements/Document.class.php b/core/Elements/Document.class.php index 395da97..72a159c 100644 --- a/core/Elements/Document.class.php +++ b/core/Elements/Document.class.php @@ -2,33 +2,39 @@ namespace Elements; +use Configuration\Settings; use Driver\SQL\SQL; +use Objects\Router\Router; use Objects\User; abstract class Document { - protected User $user; + protected Router $router; protected bool $databaseRequired; private bool $cspEnabled; private ?string $cspNonce; private array $cspWhitelist; private string $domain; - public function __construct(User $user) { - $this->user = $user; + public function __construct(Router $router) { + $this->router = $router; $this->cspEnabled = false; $this->cspNonce = null; $this->databaseRequired = true; $this->cspWhitelist = []; - $this->domain = $user->getConfiguration()->getSettings()->getBaseUrl(); - } - - public function getSQL(): ?SQL { - return $this->user->getSQL(); + $this->domain = $this->getSettings()->getBaseUrl(); } public function getUser(): User { - return $this->user; + return $this->router->getUser(); + } + + public function getSQL(): ?SQL { + return $this->getUser()->getSQL(); + } + + public function getSettings(): Settings { + return $this->getUser()->getConfiguration()->getSettings(); } public function getCSPNonce(): ?string { @@ -44,13 +50,17 @@ abstract class Document { $this->cspNonce = generateRandomString(16, "base62"); } + public function getRouter(): Router { + return $this->router; + } + protected function addCSPWhitelist(string $path) { $this->cspWhitelist[] = $this->domain . $path; } public function getCode(array $params = []): string { if ($this->databaseRequired) { - $sql = $this->user->getSQL(); + $sql = $this->getSQL(); if (is_null($sql)) { die("Database is not configured yet."); } else if (!$sql->isConnected()) { @@ -70,7 +80,7 @@ abstract class Document { "img-src 'self' data:", "script-src $cspWhiteList 'nonce-$this->cspNonce'" ]; - if ($this->user->getConfiguration()->getSettings()->isRecaptchaEnabled()) { + if ($this->getSettings()->isRecaptchaEnabled()) { $csp[] = "frame-src https://www.google.com/ 'self'"; } diff --git a/core/Elements/HtmlDocument.class.php b/core/Elements/HtmlDocument.class.php index 5aea669..74839c5 100644 --- a/core/Elements/HtmlDocument.class.php +++ b/core/Elements/HtmlDocument.class.php @@ -2,7 +2,7 @@ namespace Elements; -use Objects\User; +use Objects\Router\Router; class HtmlDocument extends Document { @@ -10,8 +10,8 @@ class HtmlDocument extends Document { protected Body $body; private ?string $activeView; - public function __construct(User $user, $headClass, $bodyClass, ?string $view = NULL) { - parent::__construct($user); + public function __construct(Router $router, $headClass, $bodyClass, ?string $view = NULL) { + parent::__construct($router); $this->head = $headClass ? new $headClass($this) : null; $this->body = $bodyClass ? new $bodyClass($this) : null; $this->activeView = $view; @@ -49,7 +49,7 @@ class HtmlDocument extends Document { return $this->activeView; } - function getCode(): string { + function getCode(array $params = []): string { parent::getCode(); @@ -65,7 +65,7 @@ class HtmlDocument extends Document { } $head = $this->head->getCode(); - $lang = $this->user->getLanguage()->getShortCode(); + $lang = $this->getUser()->getLanguage()->getShortCode(); $html = ""; $html .= ""; diff --git a/core/Elements/TemplateDocument.class.php b/core/Elements/TemplateDocument.class.php index e294002..84aa2c4 100644 --- a/core/Elements/TemplateDocument.class.php +++ b/core/Elements/TemplateDocument.class.php @@ -2,7 +2,7 @@ namespace Elements; -use Objects\User; +use Objects\Router\Router; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -17,8 +17,8 @@ class TemplateDocument extends Document { private FilesystemLoader $twigLoader; protected string $title; - public function __construct(User $user, string $templateName, array $params = []) { - parent::__construct($user); + public function __construct(Router $router, string $templateName, array $params = []) { + parent::__construct($router); $this->title = ""; $this->templateName = $templateName; $this->parameters = $params; @@ -46,15 +46,16 @@ class TemplateDocument extends Document { public function renderTemplate(string $name, array $params = []): string { try { + $user = $this->getUser(); $params["user"] = [ - "lang" => $this->user->getLanguage()->getShortCode(), - "loggedIn" => $this->user->isLoggedIn(), - "session" => (!$this->user->isLoggedIn() ? null : [ - "csrfToken" => $this->user->getSession()->getCsrfToken() + "lang" => $user->getLanguage()->getShortCode(), + "loggedIn" => $user->isLoggedIn(), + "session" => (!$user->isLoggedIn() ? null : [ + "csrfToken" => $user->getSession()->getCsrfToken() ]) ]; - $settings = $this->user->getConfiguration()->getSettings(); + $settings = $this->getSettings(); $params["site"] = [ "name" => $settings->getSiteName(), "baseUrl" => $settings->getBaseUrl(), diff --git a/core/Logs/.gitignore b/core/Logs/.gitignore new file mode 100644 index 0000000..1d295f6 --- /dev/null +++ b/core/Logs/.gitignore @@ -0,0 +1,2 @@ +* +!.htaccess \ No newline at end of file diff --git a/core/Logs/.htaccess b/core/Logs/.htaccess new file mode 100644 index 0000000..47a6aee --- /dev/null +++ b/core/Logs/.htaccess @@ -0,0 +1 @@ +DENY FROM ALL \ No newline at end of file diff --git a/core/Objects/Router/AbstractRoute.class.php b/core/Objects/Router/AbstractRoute.class.php index 925d2e5..c142d33 100644 --- a/core/Objects/Router/AbstractRoute.class.php +++ b/core/Objects/Router/AbstractRoute.class.php @@ -58,14 +58,13 @@ abstract class AbstractRoute { $patternOffset = 0; # /test/param/optional/123 - $urlParts = explode("/", $url); + $urlParts = explode("/", Router::cleanURL($url)); $countUrl = count($urlParts); $urlOffset = 0; $params = []; for (; $patternOffset < $countPattern; $patternOffset++) { - - if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) { + if (!preg_match("/^{([^:]+)(:(.*?)(\?)?)?}$/", $patternParts[$patternOffset], $match)) { // not a parameter? check if it matches if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) { @@ -73,27 +72,32 @@ abstract class AbstractRoute { } $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); + $paramName = $match[1]; + if (isset($match[2])) { + $paramType = self::parseParamType($match[3]) ?? Parameter::TYPE_MIXED; + $paramOptional = !empty($match[4] ?? null); + } else { + $paramType = Parameter::TYPE_MIXED; + $paramOptional = false; } - $paramType = self::parseParamType($paramType); + $parameter = new Parameter($paramName, $paramType, $paramOptional); if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") { - if ($paramOptional) { - $param = $urlParts[$urlOffset] ?? null; - if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) { - return false; + if ($parameter->optional) { + $value = $urlParts[$urlOffset] ?? null; + if ($value === null || $value === "") { + $params[$paramName] = null; + } else { + if (!$parameter->parseParam($value)) { + return false; + } else { + $params[$paramName] = $parameter->value; + } } - $params[$paramName] = $param; if ($urlOffset < $countUrl) { $urlOffset++; } @@ -101,13 +105,13 @@ abstract class AbstractRoute { return false; } } else { - $param = $urlParts[$urlOffset]; - if ($paramType !== null && Parameter::parseType($param) !== $paramType) { + $value = $urlParts[$urlOffset]; + if (!$parameter->parseParam($value)) { return false; + } else { + $params[$paramName] = $parameter->value; + $urlOffset++; } - - $params[$paramName] = $param; - $urlOffset++; } } } diff --git a/core/Objects/Router/ApiRoute.class.php b/core/Objects/Router/ApiRoute.class.php index af3b667..b8248f9 100644 --- a/core/Objects/Router/ApiRoute.class.php +++ b/core/Objects/Router/ApiRoute.class.php @@ -16,9 +16,9 @@ class ApiRoute extends AbstractRoute { $user = $router->getUser(); if (empty($params["endpoint"])) { header("Content-Type: text/html"); - $document = new \Elements\TemplateDocument($user, "swagger.twig"); + $document = new \Elements\TemplateDocument($router, "swagger.twig"); return $document->getCode(); - } else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $params["endpoint"])) { + } else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) { http_response_code(400); $response = createError("Invalid Method"); } else { diff --git a/core/Objects/Router/DocumentRoute.class.php b/core/Objects/Router/DocumentRoute.class.php index 61f9dc0..a24af64 100644 --- a/core/Objects/Router/DocumentRoute.class.php +++ b/core/Objects/Router/DocumentRoute.class.php @@ -60,7 +60,7 @@ class DocumentRoute extends AbstractRoute { } try { - $args = array_merge([$router->getUser()], $this->args); + $args = array_merge([$router], $this->args); $document = $this->reflectionClass->newInstanceArgs($args); return $document->getCode($params); } catch (\ReflectionException $e) { diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index cca781e..ca9cdde 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -3,9 +3,17 @@ FROM php:7.4-fpm WORKDIR "/application" RUN mkdir -p /application/core/Configuration RUN chown -R www-data:www-data /application -RUN apt-get update -y && apt-get install libyaml-dev libzip-dev -y && apt-get clean && \ - pecl install yaml && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini && \ - docker-php-ext-enable yaml -RUN docker-php-ext-install mysqli zip + +# YAML + dev dependencies +RUN apt-get update -y && apt-get install libyaml-dev libzip-dev libgmp-dev -y && apt-get clean && \ + pecl install yaml && docker-php-ext-enable yaml + +# Runkit (no stable release available) +RUN pecl install runkit7-4.0.0a3 && docker-php-ext-enable runkit7 && \ + echo "runkit.internal_override=1" >> /usr/local/etc/php/conf.d/docker-php-ext-runkit7.ini + +# mysqli, zip, gmp +RUN docker-php-ext-install mysqli zip gmp + COPY --from=composer /usr/bin/composer /usr/bin/composer USER www-data \ No newline at end of file diff --git a/index.php b/index.php index 1223583..87e7e55 100644 --- a/index.php +++ b/index.php @@ -27,14 +27,14 @@ $settings = $config->getSettings(); $installation = !$sql || ($sql->isConnected() && !$settings->isInstalled()); $requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"]; -$requestedUri = Router::cleanURL($requestedUri); if ($installation) { + $requestedUri = Router::cleanURL($requestedUri); if ($requestedUri !== "" && $requestedUri !== "index.php") { $response = "Redirecting to /"; header("Location: /"); } else { - $document = new Documents\Install($user); + $document = new Documents\Install(new Router($user)); $response = $document->getCode(); } } else { diff --git a/test/Request.test.php b/test/Request.test.php index 44c7fed..a93a48e 100644 --- a/test/Request.test.php +++ b/test/Request.test.php @@ -21,7 +21,7 @@ function __new_header_impl(string $line) { RequestTest::$SENT_HEADERS[$key] = $value; } -function __new_http_response_code_impl(int $code) { +function __new_http_response_code_impl($code) { RequestTest::$SENT_STATUS_CODE = $code; } diff --git a/test/Router.test.php b/test/Router.test.php index 62e5fef..c47ed12 100644 --- a/test/Router.test.php +++ b/test/Router.test.php @@ -1,19 +1,20 @@ match("/"); $this->assertEquals([], $paramsEmpty); - $params1 = (new EmptyRoute("/:param"))->match("/test"); + $params1 = (new EmptyRoute("/{param}"))->match("/test"); $this->assertEquals(["param" => "test"], $params1); - $params2 = (new EmptyRoute("/:param1/:param2"))->match("/test/123"); + $params2 = (new EmptyRoute("/{param1}/{param2}"))->match("/test/123"); $this->assertEquals(["param1" => "test", "param2" => "123"], $params2); - $paramOptional1 = (new EmptyRoute("/:optional1?"))->match("/"); + $paramOptional1 = (new EmptyRoute("/{optional1:?}"))->match("/"); $this->assertEquals(["optional1" => null], $paramOptional1); - $paramOptional2 = (new EmptyRoute("/:optional2?"))->match("/yes"); + $paramOptional2 = (new EmptyRoute("/{optional2:?}"))->match("/yes"); $this->assertEquals(["optional2" => "yes"], $paramOptional2); - $paramOptional3 = (new EmptyRoute("/:optional3?/:optional4?"))->match("/1/2"); + $paramOptional3 = (new EmptyRoute("/{optional3:?}/{optional4:?}"))->match("/1/2"); $this->assertEquals(["optional3" => "1", "optional4" => "2"], $paramOptional3); - $mixedRoute = new EmptyRoute("/:optional5?/:notOptional"); + $mixedRoute = new EmptyRoute("/{optional5:?}/{notOptional}"); $paramMixed1 = $mixedRoute->match("/3/4"); $this->assertEquals(["optional5" => "3", "notOptional" => "4"], $paramMixed1); } public function testMixedRoute() { - $mixedRoute1 = new EmptyRoute("/:param/static"); + $mixedRoute1 = new EmptyRoute("/{param}/static"); $this->assertEquals(["param" => "yes"], $mixedRoute1->match("/yes/static")); - $mixedRoute2 = new EmptyRoute("/static/:param"); + $mixedRoute2 = new EmptyRoute("/static/{param}"); $this->assertEquals(["param" => "yes"], $mixedRoute2->match("/static/yes")); } public function testEmptyRoute() { - $router = new Objects\Router(self::$USER); $emptyRoute = new EmptyRoute("/"); - $this->assertEquals("", $emptyRoute->call($router, [])); + $this->assertEquals("", $emptyRoute->call(RouterTest::$ROUTER, [])); + } + + public function testTypedParamRoutes() { + $intParamRoute = new EmptyRoute("/{param:int}"); + $this->assertFalse($intParamRoute->match("/test")); + $this->assertEquals(["param" => 123], $intParamRoute->match("/123")); + + $floatRoute = new EmptyRoute("/{param:float}"); + $this->assertFalse($floatRoute->match("/test")); + $this->assertEquals(["param" => 1.23], $floatRoute->match("/1.23")); + + $boolRoute = new EmptyRoute("/{param:bool}"); + $this->assertFalse($boolRoute->match("/test")); + $this->assertEquals(["param" => true], $boolRoute->match("/true")); + $this->assertEquals(["param" => false], $boolRoute->match("/false")); + + $mixedRoute = new EmptyRoute("/static/{param:int}/{optional:float?}"); + $this->assertFalse($mixedRoute->match("/static")); + $this->assertFalse($mixedRoute->match("/static/abc")); + $this->assertEquals(["param" => 123, "optional" => null], $mixedRoute->match("/static/123")); + $this->assertEquals(["param" => 123, "optional" => 4.56], $mixedRoute->match("/static/123/4.56")); } } \ No newline at end of file