diff options
Diffstat (limited to 'src/router.php')
-rw-r--r-- | src/router.php | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/src/router.php b/src/router.php new file mode 100644 index 0000000..141e7cf --- /dev/null +++ b/src/router.php @@ -0,0 +1,236 @@ +<?php +/// CRIMSON --- A simple PHP framework. +/// Copyright © 2024 Freya Murphy <contact@freyacat.org> +/// +/// This file is part of CRIMSON. +/// +/// CRIMSON is free software; you can redistribute it and/or modify it +/// under the terms of the GNU General Public License as published by +/// the Free Software Foundation; either version 3 of the License, or (at +/// your option) any later version. +/// +/// CRIMSON is distributed in the hope that it will be useful, but +/// WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU General Public License for more details. +/// +/// You should have received a copy of the GNU General Public License +/// along with CRIMSON. If not, see <http://www.gnu.org/licenses/>. + +class Router extends Base { + + // if the website is ready to accept requests + // (protects against users requesting unready resources) + private bool $ready; + // if the router is currently in an error handler + private bool $in_err; + // The parsed request. Contains the following fields. + // + // NAME | TYPE | DEFAULT | DESCRIPTION + // ------------------------------------------------------------ + // app | string | 'index' | The first part of the uri, selects which + // | | | controller will be called. + // slug | string | 'index' | The second part of the uri, slects + // | | | which method will be called. + // args | array | [] | Rest of uri parts. Arguments passed to + // | | | the method. + // uri | array? | | From PHP's parse_url($uri_str). + // uri_str | string | | The URI given in the HTML request. + // method | string | | The HTTP method. + // ip | string | | The requesting IP. + public final array $req; + + /** + * Creates the crimson router. + * + * => Checks if crimson is ready to route requests. + * => Defines self as ROUTER. + */ + function __construct() + { + // are we ready? + $this->ready = $this->ready_check(); + // get request + $this->in_err = FALSE; + $this->req = $this->get_req(); + // ROUTER is used by Controller in error($code) function + define('ROUTER', $this); + // fail if URI is invalid (get_req does not check this) + if (!$this->req['uri']) + $this->handle_error(400); // does not return! + } + + /** + * Cheks if crimson is ready to route requests. + * => Checks if the database is ready (if enabled). + */ + private function ready_check(): bool { + // assume we are ready unless told otherwise + $ready = TRUE; + // check db + if ($ready && getenv('POSTGRES_ENABLED') === 'true') { + $ready = file_exists('/var/run/crimson/db_ready'); + } + // return result + return $ready; + } + + /** + * @param string $path - the current request path + * Gets the current route + * @return array<string,mixed> + */ + private function get_req_route($path): array + { + // trim the path + $path = trim($path); + // remove first '/' + $path = substr($path, 1); + + // get modified route + $routes = CONFIG['routes']; + foreach ($routes as $key => $value) { + $key = "/^{$key}$/"; + if (!preg_match($key, $path, $matches)) + continue; + + $path = $value; + + for ($i = 1; $i < count($matches); $i++) { + $path = str_replace( + "\\{$i}", + $matches[$i], + $path); + } + + break; + } + + // get path parts + $parts = explode('/', $path); + if ($path == '') + $parts = []; + // get the length + $len = count($parts); + // get route info + $route = array(); + $route['app'] = $len > 0 ? $parts[0] : 'index'; + $route['slug'] = $len > 1 ? $parts[1] : 'index'; + $route['args'] = array_slice($parts, 2); + + return $route; + } + + /** + * Gets the users ip + */ + private function get_ip(): ?string + { + $headers = array ( + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + 'HTTP_X_REAL_IP', + 'REMOTE_ADDR' + ); + foreach ($headers as $header) { + if (isset($_SERVER[$header])) + return $_SERVER[$header]; + } + return NULL; + } + + /** + * Gets the curret request info. + * Does not fail in invalid uri. Must be handeled by + * caller function. + * @return array<string,mixed> + */ + private function get_req(): array + { + $method = $_SERVER['REQUEST_METHOD']; + $uri_str = $_SERVER['REQUEST_URI']; + $uri = parse_url($uri_str); + + $path = ''; + if ($uri && array_key_exists('path', $uri)) + $path = $uri['path']; + + return array_merge( + array( + 'uri' => $uri, + 'uri_str' => $uri_str, + 'method' => $method, + 'ip' => $this->get_ip() + ), + $this->get_req_route($path), + ); + } + + /** + * Handles a router error code + * @param int $code - the http error code + */ + public function handle_error(int $code): never + { + // if in_err is set RIGHT NOW, this means the user specified + // error hook errored itself. To prevent error recursion we do + // not want to run it again! + $force_builtin = $this->in_err; + // Sets the in_err catch to true, read comment above to why. + $this->in_err = TRUE; + CRIMSON_HOOK('error', [$this->req, $code], $force_builtin); + // error hook is type never, but CRIMSON_HOOK is type void + CRIMSON_DIE(); + } + + /** + * @param array $req + * @param array<int,mixed> $req + */ + public function handle_req(): never + { + // block requests if we are not ready + if ($this->ready === FALSE) + $this->handle_error(503); + + // run pre route hook + CRIMSON_HOOK('pre_route', [$this]); + + // load the controller + $controller = $this->load_controller($this->req['app']); + if (!$controller) + $this->handle_error(404); + + // find the function that matches our request + // format: /controller/fn_name/fn_arg1/fn_arg2/.../fn_argN + $handler = NULL; + try { + $cls = new ReflectionClass($controller); + $mds = $cls->getMethods(ReflectionMethod::IS_PUBLIC); + foreach ($mds as $md) { + if ($md->name !== $this->req['slug']) + continue; + if (count($md->getParameters()) != + count($this->req['args'])) + continue; + $handler = $md; + break; + } + } catch (Exception $_e) {} + + // return 404 if no handler found + if (!$handler) + $this->handle_error(404); + + try { + $handler->invokeArgs($controller, $this->req['args']); + } catch (Exception $_e) {}; + + // sanity check + CRIMSON_DIE(); + } + +} |