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