Browse Source

Router Update + Bugfix

Roman 1 year ago
parent
commit
1fb875fb2c

+ 6 - 1
.htaccess

@@ -11,4 +11,9 @@ RewriteEngine On
 RewriteOptions AllowNoSlash
 RewriteRule ^((\.idea|\.git|src|test|core|docker|files)(/.*)?)$ /index.php?site=$1 [L,QSA]
 
-FallbackResource /index.php
+FallbackResource /index.php
+
+ErrorDocument 400 /index.php?error=400
+ErrorDocument 403 /index.php?error=403
+ErrorDocument 404 /index.php?error=404
+ErrorDocument 500 /index.php?error=500

+ 3 - 1
.idea/php.xml

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="PhpProjectSharedConfiguration" php_language_level="7.4" />
+  <component name="PhpProjectSharedConfiguration" php_language_level="8.0">
+    <option name="suggestChangeDefaultLanguageLevel" value="false" />
+  </component>
 </project>

+ 1 - 123
adminPanel/src/views/settings.js

@@ -3,11 +3,6 @@ import {Link} from "react-router-dom";
 import Alert from "../elements/alert";
 import {Collapse} from "react-collapse/lib/Collapse";
 import Icon from "../elements/icon";
-import { EditorState, ContentState, convertToRaw } from 'draft-js'
-import { Editor } from 'react-draft-wysiwyg'
-import draftToHtml from 'draftjs-to-html';
-import htmlToDraft from 'html-to-draftjs';
-import sanitizeHtml from 'sanitize-html'
 import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
 import ReactTooltip from "react-tooltip";
 
@@ -36,15 +31,6 @@ export default class Settings extends React.Component {
                 unsavedMailSettings: false,
                 keys: ["mail_enabled", "mail_host", "mail_port", "mail_username", "mail_password", "mail_from"]
             },
-            messages: {
-                alerts: [],
-                isOpen: true,
-                isSaving: false,
-                isResetting: false,
-                editor: EditorState.createEmpty(),
-                isEditing: null,
-                keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"]
-            },
             recaptcha: {
                 alerts: [],
                 isOpen: true,
@@ -77,7 +63,6 @@ export default class Settings extends React.Component {
         key = key.trim();
         return this.state.general.keys.includes(key)
             || this.state.mail.keys.includes(key)
-            || this.state.messages.keys.includes(key)
             || this.state.recaptcha.keys.includes(key)
             || this.hiddenKeys.includes(key);
     }
@@ -345,6 +330,7 @@ export default class Settings extends React.Component {
                         {this.state.mail.isSending ?
                             <span>Sending&nbsp;<Icon icon={"circle-notch"}/></span> : "Send Mail"}
                     </button>
+
                     <div className={"col-10"}>
                         {this.state.mail.unsavedMailSettings ?
                             <span className={"text-red"}>You need to save your mail settings first.</span> : null}
@@ -354,63 +340,6 @@ export default class Settings extends React.Component {
         </>
     }
 
-    getMessagesForm() {
-
-        const editor = <Editor
-            editorState={this.state.messages.editor}
-            onEditorStateChange={this.onEditorStateChange.bind(this)}
-        />;
-
-        let messageTemplates = {
-            "message_confirm_email": "Confirm E-Mail Message",
-            "message_accept_invite": "Accept Invitation Message",
-            "message_reset_password": "Reset Password Message",
-        };
-
-        let formGroups = [];
-        for (let key in messageTemplates) {
-            let title = messageTemplates[key];
-            if (this.state.messages.isEditing === key) {
-                formGroups.push(
-                    <div className={"form-group"} key={"group-" + key}>
-                        <label htmlFor={key}>
-                            { title }
-                            <ReactTooltip id={"tooltip-" + key} />
-                            <Icon icon={"times"} className={"ml-2 text-danger"} style={{cursor: "pointer"}}
-                                  onClick={() => this.closeEditor(false)} data-type={"error"}
-                                  data-tip={"Discard Changes"} data-place={"top"} data-effect={"solid"}
-                                  data-for={"tooltip-" + key}
-                            />
-                            <Icon icon={"check"} className={"ml-2 text-success"} style={{cursor: "pointer"}}
-                                  onClick={() => this.closeEditor(true)} data-type={"success"}
-                                  data-tip={"Save Changes"} data-place={"top"} data-effect={"solid"}
-                                  data-for={"tooltip-" + key}
-                            />
-                        </label>
-                        { editor }
-                    </div>
-                );
-            } else {
-                formGroups.push(
-                    <div className={"form-group"} key={"group-" + key}>
-                        <ReactTooltip id={"tooltip-" + key} />
-                        <label htmlFor={key}>
-                            { title }
-                            <Icon icon={"pencil-alt"} className={"ml-2"} style={{cursor: "pointer"}}
-                                  onClick={() => this.openEditor(key)} data-type={"info"}
-                                  data-tip={"Edit Template"} data-place={"top"} data-effect={"solid"}
-                                  data-for={"tooltip-" + key}
-                            />
-                        </label>
-                        <div className={"p-2 text-black"} style={{backgroundColor: "#d2d6de"}} dangerouslySetInnerHTML={{ __html: sanitizeHtml(this.state.settings[key] ?? "") }} />
-                    </div>
-                );
-            }
-        }
-
-        return formGroups;
-    }
-
     getRecaptchaForm() {
         return <>
             <div className={"form-group mt-2"}>
@@ -520,7 +449,6 @@ export default class Settings extends React.Component {
         const categories = {
             "general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
             "mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
-            "messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
             "recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
             "uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
         };
@@ -557,16 +485,6 @@ export default class Settings extends React.Component {
         </>
     }
 
-    onEditorStateChange(editorState) {
-        this.setState({
-            ...this.state,
-            messages: {
-                ...this.state.messages,
-                editor: editorState
-            }
-        });
-    };
-
     onChangeValue(event) {
         const target = event.target;
         const name = target.name;
@@ -620,8 +538,6 @@ export default class Settings extends React.Component {
 
                     if (category === "mail") {
                         categoryUpdated.unsavedMailSettings = false;
-                    } else if (category === "messages") {
-                        categoryUpdated.isEditing = null;
                     }
                 }
 
@@ -635,10 +551,6 @@ export default class Settings extends React.Component {
     onSave(category) {
         this.setState({...this.state, [category]: {...this.state[category], isSaving: true}});
 
-        if (category === "messages" && this.state.messages.isEditing) {
-            this.closeEditor(true, () => this.onSave(category));
-        }
-
         let values = {};
         if (category === "uncategorised") {
             for (let prop of this.state.uncategorised.settings) {
@@ -705,38 +617,4 @@ export default class Settings extends React.Component {
             this.setState({...this.state, mail: newState});
         });
     }
-
-    closeEditor(save, callback = null) {
-        if (this.state.messages.isEditing) {
-            const key = this.state.messages.isEditing;
-            let newState = { ...this.state, messages: {...this.state.messages, isEditing: null }};
-
-            if (save) {
-                newState.settings = {
-                    ...this.state.settings,
-                    [key]: draftToHtml(convertToRaw(this.state.messages.editor.getCurrentContent())),
-                };
-            }
-
-            callback = callback || function () { };
-            this.setState(newState, callback);
-        }
-    }
-
-    openEditor(message) {
-        this.closeEditor(true);
-        const contentBlock = htmlToDraft(this.state.settings[message] ?? "");
-        if (contentBlock) {
-            const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
-            const editorState = EditorState.createWithContent(contentState);
-            this.setState({
-                ...this.state,
-                messages: {
-                    ...this.state.messages,
-                    isEditing: message,
-                    editor: editorState
-                }
-            });
-        }
-    }
 }

+ 4 - 3
cli.php

@@ -42,7 +42,7 @@ function getDatabaseConfig(): ConnectionData {
 $config = new Configuration();
 $database = $config->getDatabase();
 if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
-  if (count($argv) < 2 || $argv[1] !== "db") {
+  if (count($argv) < 3 || $argv[1] !== "db" || !in_array($argv[2], ["shell", "import", "export"])) {
     $command = array_merge(["docker", "exec", "-it", "php", "php"], $argv);
     $proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes, "/application");
     exit(proc_close($proc));
@@ -464,11 +464,12 @@ function onRoutes(array $argv) {
       _exit("Error fetching routes: " . $req->getLastError());
     } else {
       $routes = $req->getResult()["routes"];
-      $head = ["uid", "request", "action", "target", "extra", "active"];
+      $head = ["uid", "request", "action", "target", "extra", "active", "exact"];
 
       // strict boolean
       foreach ($routes as &$route) {
         $route["active"] = $route["active"] ? "true" : "false";
+        $route["exact"] = $route["exact"] ? "true" : "false";
       }
 
       printTable($head, $routes);
@@ -482,7 +483,7 @@ function onRoutes(array $argv) {
       "request" => $argv[3],
       "action" => $argv[4],
       "target" => $argv[5],
-      "extra" => $argv[6] ?? ""
+      "extra" => $argv[7] ?? "",
     );
 
     $req  = new Api\Routes\Add($user);

+ 2 - 2
core/Api/MailAPI.class.php

@@ -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;
       }
 

+ 8 - 5
core/Api/RoutesAPI.class.php

@@ -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;

+ 4 - 0
core/Api/UserAPI.class.php

@@ -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);

+ 0 - 390
core/Objects/Router.class.php

@@ -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()]);
-      }
-    }
-  }
-}

+ 121 - 0
core/Objects/Router/AbstractRoute.class.php

@@ -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;
+  }
+}

+ 60 - 0
core/Objects/Router/ApiRoute.class.php

@@ -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;
+  }
+}

+ 70 - 0
core/Objects/Router/DocumentRoute.class.php

@@ -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()]);
+    }
+  }
+}

+ 14 - 0
core/Objects/Router/EmptyRoute.class.php

@@ -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 "";
+  }
+}

+ 25 - 0
core/Objects/Router/RedirectRoute.class.php

@@ -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]);
+  }
+}

+ 128 - 0
core/Objects/Router/Router.class.php

@@ -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);
+  }
+}

+ 24 - 0
core/Objects/Router/StaticFileRoute.class.php

@@ -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]);
+  }
+}

+ 24 - 0
core/Objects/Router/StaticRoute.class.php

@@ -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]);
+  }
+}

+ 4 - 1
core/Templates/account/login.twig

@@ -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>

+ 1 - 0
core/Templates/error_document.twig

@@ -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>

+ 6 - 0
docker/nginx/site.conf

@@ -4,6 +4,12 @@ server {
 	access_log /var/log/nginx/access.log;
 	root /application;
 
+    # rewrite error codes
+    error_page   400          /index.php?error=400;
+    error_page   403          /index.php?error=403;
+    error_page   404          /index.php?error=404;
+    error_page   500          /index.php?error=500;
+
 	# rewrite api
 	rewrite ^/api(/.*)$ /index.php?api=$1;
 

+ 0 - 1
img/icons/files/.htaccess

@@ -1 +0,0 @@
-php_flag engine on

+ 33 - 88
index.php

@@ -12,13 +12,12 @@ if (is_file("MAINTENANCE") && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '
   die();
 }
 
-use Api\Request;
 use Configuration\Configuration;
-use Objects\Router;
+use Objects\Router\Router;
 
 if (!is_readable(getClassPath(Configuration::class))) {
   header("Content-Type: application/json");
-  die(json_encode(array( "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." )));
+  die(json_encode([ "success" => false, "msg" => "Configuration class is not readable, check permissions before proceeding." ]));
 }
 
 $config = new Configuration();
@@ -27,103 +26,49 @@ $sql    = $user->getSQL();
 $settings = $config->getSettings();
 $installation = !$sql || ($sql->isConnected() && !$settings->isInstalled());
 
-// API routes, prefix: /api/
-// TODO: move this to Router?
-if (isset($_GET["api"]) && is_string($_GET["api"])) {
-  $isApiResponse = true;
-  if ($installation) {
-    $response = createError("Not installed");
+$requestedUri = $_GET["site"] ?? $_GET["api"] ?? $_SERVER["REQUEST_URI"];
+$requestedUri = Router::cleanURL($requestedUri);
+
+if ($installation) {
+  if ($requestedUri !== "" && $requestedUri !== "index.php") {
+    $response = "Redirecting to <a href=\"/\">/</a>";
+    header("Location: /");
   } else {
-    $apiFunction = $_GET["api"];
-    if (empty($apiFunction) || $apiFunction === "/") {
-      $document = new \Elements\TemplateDocument($user, "swagger.twig");
-      $response = $document->getCode();
-      $isApiResponse = false;
-    } else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $apiFunction)) {
-      http_response_code(400);
-      $response = createError("Invalid Method");
-    } else {
-      $apiFunction = array_filter(array_map('ucfirst', explode("/", $apiFunction)));
-      if (count($apiFunction) > 1) {
-        $parentClass = "\\Api\\" . reset($apiFunction) . "API";
-        $apiClass = "\\Api\\" . implode("\\", $apiFunction);
-      } else {
-        $apiClass = "\\Api\\" . implode("\\", $apiFunction);
-        $parentClass = $apiClass;
-      }
+    $document = new Documents\Install($user);
+    $response = $document->getCode();
+  }
+} else {
 
-      try {
-        $file = getClassPath($parentClass);
-        if(!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
-          http_response_code(404);
-          $response = createError("Not found");
-        } else {
-          $parentClass = new ReflectionClass($parentClass);
-          $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));
-            $success = $request->execute();
-            $msg = $request->getLastError();
-            $response = $request->getJsonResult();
-          }
-        }
-      } catch (ReflectionException $e) {
-        $response = createError("Error instantiating class: $e");
-      }
+  $router = null;
+  $routerCacheClass = '\Cache\RouterCache';
+  $routerCachePath = getClassPath($routerCacheClass);
+  if (is_file($routerCachePath)) {
+    @include_once $routerCachePath;
+    if (class_exists($routerCacheClass)) {
+      $router = new $routerCacheClass($user);
     }
+  }
 
-    if ($isApiResponse) {
-      header("Content-Type: application/json");
+  if ($router === null) {
+    $req = new \Api\Routes\GenerateCache($user);
+    if ($req->execute()) {
+      $router = $req->getRouter();
     } else {
-      header("Content-Type: text/html");
+      $message = "Unable to generate router cache: " . $req->getLastError();
+      $response = (new Router($user))->returnStatusCode(500, [ "message" => $message ]);
     }
   }
-} else {
-
-  // all other routes
-  $requestedUri = $_GET["site"] ?? $_SERVER["REQUEST_URI"];
-  $requestedUri = Router::cleanURL($requestedUri);
 
-  if ($installation) {
-    if ($requestedUri !== "" && $requestedUri !== "index.php") {
-      $response = "Redirecting to <a href=\"/\">/</a>";
-      header("Location: /");
+  if ($router !== null) {
+    if (!isset($_GET["site"]) && isset($_GET["error"]) &&
+      is_string($_GET["error"]) && preg_match("^\d+$", $_GET["error"])) {
+      $response = $router->returnStatusCode(intval($_GET["error"]));
     } else {
-      $document = new Documents\Install($user);
-      $response = $document->getCode();
-    }
-  } else {
-
-    $router = null;
-
-    $routerCacheClass = '\Cache\RouterCache';
-    $routerCachePath = getClassPath($routerCacheClass);
-    if (is_file($routerCachePath)) {
-      @include_once $routerCachePath;
-      if (class_exists($routerCacheClass)) {
-        $router = new $routerCacheClass($user);
-      }
-    }
-
-    if ($router === null) {
-      $req = new \Api\Routes\GenerateCache($user);
-      if ($req->execute()) {
-        $router = $req->getRouter();
-      } else {
-        $message = "Unable to generate router cache: " . $req->getLastError();
-        $response = (new Router($user))->returnStatusCode(500, [ "message" => $message ]);
-      }
-    }
-
-    if ($router !== null) {
       $response = $router->run($requestedUri);
     }
-
-    $user->processVisit();
   }
+
+  $user->processVisit();
 }
 
 $user->sendCookies();

File diff suppressed because it is too large
+ 0 - 0
js/admin.min.js


Some files were not shown because too many files changed in this diff