summaryrefslogtreecommitdiff
path: root/src/router.php
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2024-12-23 10:39:16 -0500
committerFreya Murphy <freya@freyacat.org>2024-12-23 10:39:16 -0500
commitde9cae795f93d03e68d965c59af4b21d96df4ec7 (patch)
treead4f903c04630b3b92e2b9b5d06d5b8647d299bb /src/router.php
parentlicense (diff)
downloadcrimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.tar.gz
crimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.tar.bz2
crimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.zip
initial
Diffstat (limited to 'src/router.php')
-rw-r--r--src/router.php236
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();
+ }
+
+}