/// /// 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 . 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 */ 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 */ 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 $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(); } }