diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/_base.php | 309 | ||||
-rw-r--r-- | src/_controller.php | 66 | ||||
-rw-r--r-- | src/_model.php | 63 | ||||
-rw-r--r-- | src/config.php | 75 | ||||
-rw-r--r-- | src/index.php | 68 | ||||
-rw-r--r-- | src/lib/database.php | 337 | ||||
-rw-r--r-- | src/lib/error.php | 218 | ||||
-rw-r--r-- | src/lib/hooks.php | 178 | ||||
-rw-r--r-- | src/lib/html.php | 95 | ||||
-rw-r--r-- | src/lib/lang.php | 186 | ||||
-rw-r--r-- | src/lib/meta.php | 28 | ||||
-rw-r--r-- | src/router.php | 236 |
12 files changed, 1859 insertions, 0 deletions
diff --git a/src/_base.php b/src/_base.php new file mode 100644 index 0000000..89df718 --- /dev/null +++ b/src/_base.php @@ -0,0 +1,309 @@ +<?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/>. + +/** + * Gives access to imporant + * needed utility functions for + * accessing everything else! + */ +abstract class Base { + +// ======================================================== LOADABLE OBJECTS == + + // keep track of what has been loaded + private static array $loaded = array(); + + /** + * Loads a $type of object from a $dir with a given $name + * @param string $name - the name of the object to load + * @param string $dir - the directory theese objects are stored in + * @param string $type - the type of the object + */ + private function load_type($name, $dir, $type): object|NULL + { + + $path = $dir . '/' . $name . '.php'; + + // dont reload an ohject + if (array_key_exists($path, self::$loaded)) + return self::$loaded[$path]; + + // only load a object if it exists + if (!file_exists($path)) + return NULL; + + $parts = explode('/', $name); + $part = end($parts); + $class = ucfirst($part) . '_' . $type; + require($path); + + $ref = NULL; + try { + $ref = new ReflectionClass($class); + } catch (Exception $_e) {} + + if ($ref === NULL) + return NULL; + + $obj = $ref->newInstance(); + self::$loaded[$path] = $obj; + + return $obj; + } + + /** + * Loads a model + * @param string $name - the name of the model to load + */ + public function load_model($name): Model|NULL + { + $dir = WEB_ROOT . '/_model'; + return $this->load_type($name, $dir, 'model'); + } + + /** + * Loads a controller + * @param string $name - the name of the controller to load + */ + public function load_controller($name): Controller|NULL + { + $dir = WEB_ROOT . '/_controller'; + return $this->load_type($name, $dir, 'controller'); + } + +// ==================================================================== LANG == + + // current loaded language strings + private static array $loaded_lang = array(); + private static array $loaded_files = array(); + + /** + * Loads a php lang file into the lang array + */ + private function load_lang_file(string $file): void + { + if (isset(self::$loaded[$file])) + return; + self::$loaded[$file] = TRUE; + + $lang = self::$loaded_lang; + require($file); + self::$loaded_lang = $lang; + } + + /** + * Loads each php file lang strings in a directory + */ + private function load_lang_dir(string $dir): void + { + if ($handle = opendir($dir)) { + while (false !== ($entry = readdir($handle))) { + if ($entry === '.' || $entry === '..') + continue; + $this->load_lang_file($entry); + } + } + } + + /** + * Loads the given common lang + */ + public function load_lang(string ...$langs): array + { + $root = WEB_ROOT . '/lang'; + + foreach ($langs as $lang) { + $file = "{$root}/{$lang}.php"; + $dir = "{$root}/{$lang}"; + + if (file_exists($file)) + $this->load_lang_file($file); + else if (is_dir($dir)) + $this->load_lang_dir($dir); + + } + + return self::$loaded_lang; + } + + /** + * Returns the currently loaded lang + */ + public function get_lang(): array + { + return self::$loaded_lang; + } + +// ================================================================ DATABASE == + + // current database connection + private static ?DatabaseHelper $db = NULL; + + /** + * Loads the database + */ + public function db(): DatabaseHelper + { + if (!self::$db) + self::$db = new DatabaseHelper(); + return self::$db; + } + +// ================================================================ METADATA == + + /** + * Gets the stamp for a asset path + * @param string $path + */ + public function asset_stamp(string $path): int + { + if (ENVIRONMENT == 'devlopment') + return time(); + if (isset(FILE_TIMES[$path])) + return FILE_TIMES[$path]; + return 0; + } + + /** + * Gets a full path url from a relative path + * @param string $path + * @param bool $timestamp + */ + public function get_url(string $path, bool $timestamp = FALSE): string + { + $scheme = 'http'; + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) + $scheme = $_SERVER['HTTP_X_FORWARDED_PROTO']; + + $host = $_SERVER['HTTP_HOST']; + + if (ENVIRONMENT == 'production') { + $default = CONFIG['domain']; + $allowed = CONFIG['allowed_hosts']; + if (!is_array($allowed)) + $allowed = [$allowed]; + if (!in_array($host, $allowed)) + $host = $default; + } + + $base = CONFIG['base_path']; + $url = "{$scheme}://{$host}{$base}{$path}"; + if ($timestamp) { + $time = $this->asset_stamp($path); + $url .= "?timestamp={$time}"; + } + return $url; + } + + /** + * Loads a js html link + * @param string $path - the path to the js file + */ + public function link_js(string $path): string + { + $stamp = $this->asset_stamp("public/$path"); + $href = $this->get_url("public/{$path}?timestamp={$stamp}"); + return '<script src="'. $href .'"></script>'; + } + + /** + * Loads a css html link + * @param string $path - the path to the css file + */ + public function link_css(string $path): string + { + $stamp = $this->asset_stamp("public/$path"); + $href = $this->get_url("public/{$path}?timestamp={$stamp}"); + return '<link rel="stylesheet" href="'. $href .'">'; + } + + /** + * Loads a css html link + * @param string $path - the path to the css file + */ + public function embed_css(string $path): string + { + $file = PUBLIC_ROOT . '/' . $path; + if (file_exists($file)) { + $text = file_get_contents($file); + return "<style>{$text}</style>"; + } else { + return ""; + } + } + +// =============================================================== HTTP POST == + + /** + * Gets http POST data from $_POST if x-url-encoded, or parsed from + * php://input if AJAX. Returns FALSE if input is not a post request, + * or NULL if unable to parse request body. + */ + private function get_post_data() + { + static $data = NULL; + + if (isset($data)) + return $data; + + // not a POST request + if ($_SERVER['REQUEST_METHOD'] != 'POST') + return NULL; + + // ajax + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && + $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { + $data = json_decode(file_get_contents("php://input"), + true); + // on failure, make sure $data is FALSE and not NULL + if (!$data) + $data = FALSE; + return $data; + } + + // x-url-encoded or form-data + $data = $_POST; + return $data; + } + + /** + * Returns HTTP POST information if POST request. + * @returns $_POST if $key is not set and request is POST. + * @returns value at $key if $key is set and request is POST. + * @returns FALSE if value at $key is not set and request is POST. + * @returns FALSE if request is POST but has invalid body. + * @returns NULL if request is not POST. + */ + public function post_data(?string $key = NULL) + { + $data = $this->get_post_data(); + + if (!$data) + return $data; + + if (!$key) + return $data; + + if (!isset($data[$key])) + return FALSE; + + return $data[$key]; + } + +} diff --git a/src/_controller.php b/src/_controller.php new file mode 100644 index 0000000..6c829a0 --- /dev/null +++ b/src/_controller.php @@ -0,0 +1,66 @@ +<?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/>. + +abstract class Controller extends Base { + + // used by lib meta + public array $CRIMSON_data = array(); + + /** + * Default index for a app, empty + */ + public function index(): void {} + + /** + * Lodas a view + */ + protected function view(string $CRIMSON_name, array $data = array()): void + { + $CRIMSON_path = WEB_ROOT . '/_views/' . $CRIMSON_name . '.php'; + $this->CRIMSON_data = $data; + + if (!is_file($CRIMSON_path)) { + CRIMSON_ERROR("View '{$CRIMSON_name}' does not exist!"); + return; + } + + extract($this->CRIMSON_data); + require($CRIMSON_path); + } + + /** + * Redirectes to a link + */ + protected function redirect(string $link): never + { + header('Location: '. $link, true, 301); + CRIMSON_DIE(); + } + + /** + * Loads a erorr page with a given + * error code + */ + protected function error(int $code): never + { + // does not return + ROUTER->handle_error($code); + } + +} diff --git a/src/_model.php b/src/_model.php new file mode 100644 index 0000000..ce8dd82 --- /dev/null +++ b/src/_model.php @@ -0,0 +1,63 @@ +<?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/>. + +abstract class Model extends Base { + + private function flatten_array($arr): array { + $fn = fn($e) => is_array($e) ? $e : [$e]; + return array_merge(...array_map($fn, $arr)); + } + + protected function get_data(): ?array + { + /* return barebones base data */ + $data = array(); + $data['css'] = array(); + $data['js'] = array(); + + $style = CONFIG['style']; + $js = CONFIG['js']; + + $app = NULL; + try { + // get the class object of the child class, i.e. Blog_model + $cls = new ReflectionClass(get_class($this)); + // the name of the route is the name of the file without .php + $path = $cls->getFileName(); + $app = pathinfo($path, PATHINFO_FILENAME); + // sanity check + assert(is_string($app)); + } catch (Exception $_e) { + $app = CONTEXT['app']; + } + + $data['css'] = $this->flatten_array([ + $style[''] ?? [], + $style[$app] ?? [], + ]); + + $data['js'] = $this->flatten_array([ + $js[''] ?? [], + $js[$app] ?? [], + ]); + + return $data; + } + +} diff --git a/src/config.php b/src/config.php new file mode 100644 index 0000000..0d52d96 --- /dev/null +++ b/src/config.php @@ -0,0 +1,75 @@ +<?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/>. + +// ENVIRONMENT +// +// devlopment - do not cache any assets +// - use http host provided by user +// +// production - use generated timestamps for each file +// - hard code http host to 'domain' lang string +// +if (!defined('ENVIRONMENT')) { + if (getenv('ENVIRONMENT') !== FALSE) + define('ENVIRONMENT', getenv('ENVIRONMENT')); + else + define('ENVIRONMENT', 'devlopment'); +} + +// CONFIG +// config values needed across the website +// +// domain - the default domain for the website +// +// allowed_hosts - accepted domains to use for the website +// +// base_path - the base path the website is located at +// +// theme_color - html hex color used for browser metadata +// +// routes - array of regex keys that match the request path and +// - place it with the value if it matches +// - e.g. '' => 'home' sends / to /home +// +// style - single or list of css styles to load on specific routes +// +// js - single or list of js script to load on specific routes +// +// autoload - list of directories to autoload all PHP files in them +// +define('BASE_CONFIG', array( + /* core settings */ + 'domain' => 'localhost', + 'allowed_hosts' => ['localhost'], + 'base_path' => '/', + 'theme_color' => '#181818', + /* route overides */ + 'routes' => array(), + /* css to load on each route */ + 'style' => array(), + /* js to load on each route */ + 'js' => array(), + /* directories to autoload php code */ + 'autoload' => array(), +)); + +if (!defined('SITE_CONFIG')) + define('SITE_CONFIG', array()); + +define('CONFIG', array_merge(BASE_CONFIG, SITE_CONFIG)); diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..98b8be7 --- /dev/null +++ b/src/index.php @@ -0,0 +1,68 @@ +<?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/>. + +// ============================================================= ENVIRONMENT == + +// define folder paths +define('CRIMSON_ROOT', '/opt/crimson'); +define('PHP_ROOT', '/opt/site'); +define('WEB_ROOT', PHP_ROOT . '/web'); +define('PUBLIC_ROOT', PHP_ROOT . '/public'); + +// =============================================================== BOOTSTRAP == + +// load the config +@include(WEB_ROOT . '/config.php'); +require(CRIMSON_ROOT . '/config.php'); + +// load all core files (order matters) +require(CRIMSON_ROOT . '/lib/database.php'); +require(CRIMSON_ROOT . '/_base.php'); +require(CRIMSON_ROOT . '/_model.php'); +require(CRIMSON_ROOT . '/_controller.php'); +require(CRIMSON_ROOT . '/router.php'); +require(CRIMSON_ROOT . '/lib/error.php'); +require(CRIMSON_ROOT . '/lib/hooks.php'); +require(CRIMSON_ROOT . '/lib/lang.php'); +require(CRIMSON_ROOT . '/lib/meta.php'); +require(CRIMSON_ROOT . '/lib/html.php'); + +// autoload requested directories +foreach (CONFIG['autoload'] as $dir) + foreach (glob(WEB_ROOT . $dir . '/*.php') as $file) + require($file); + +// load file stamps on production +if (ENVIRONMENT == 'production') + require('/var/run/crimson/stamp.php'); + +// =================================================================== START == + +try { + CRIMSON_HOOK('init'); + (new Router())->handle_req(); +} catch (Error $e) { + CRIMSON_error_handler( + CRIMSON_E_FATAL_ERROR, + $e->getMessage(), + $e->getFile(), + $e->getLine(), + $e->getTrace(), + ); +} diff --git a/src/lib/database.php b/src/lib/database.php new file mode 100644 index 0000000..416ef8f --- /dev/null +++ b/src/lib/database.php @@ -0,0 +1,337 @@ +<?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/>. + +function __nullify(mixed $val): mixed +{ + if (!$val) { + return NULL; + } else { + return $val; + } +} + +class DatabaseQuery { + + private \PDO $conn; + private string $query; + + private bool $where; + private bool $set; + + private array $param; + + function __construct(\PDO $conn) + { + $this->conn = $conn; + $this->query = ''; + + $this->set = FALSE; + $this->where = FALSE; + $this->param = array(); + } + + /// + /// ARBITRARY QUERY + /// + + public function query(string $query): DatabaseQuery + { + $this->query .= $query; + return $this; + } + + /// + /// SELECT + /// + + public function select(string $select): DatabaseQuery + { + $this->query .= "SELECT $select\n"; + return $this; + } + + public function from(string $from): DatabaseQuery + { + $this->query .= "FROM $from\n"; + return $this; + } + + /// + /// INSERT + /// + + public function insert_into(string $insert, string ...$columns): DatabaseQuery + { + $this->query .= "INSERT INTO $insert\n ("; + foreach ($columns as $idx => $column) { + if ($idx !== 0) { + $this->query .= ","; + } + $this->query .= $column; + } + $this->query .= ")\n"; + return $this; + } + + public function values(mixed ...$values): DatabaseQuery + { + $this->query .= "VALUES ("; + foreach ($values as $idx => $value) { + if ($idx !== 0) { + $this->query .= ","; + } + $this->query .= "?"; + array_push($this->param, $value); + } + $this->query .= ")\n"; + return $this; + } + + /// + /// WHERE + /// + + public function where(string $cond): DatabaseQuery + { + if (!$this->where) { + $this->where = TRUE; + $this->query .= "WHERE "; + } else { + $this->query .= "AND "; + } + $this->query .= "$cond "; + return $this; + } + + /** + * @param array<mixed> $array + */ + public function where_in(string $column, array $array): DatabaseQuery + { + if (!$this->where) { + $this->where = TRUE; + $this->query .= "WHERE "; + } else { + $this->query .= "AND "; + } + if (empty($array)) { + $this->query .= "FALSE\n"; + return $this; + } + $in = $this->in($array); + $this->query .= "$column $in\n"; + return $this; + } + + private function in(array $array): string + { + $in = 'IN ('; + foreach ($array as $idx => $item) { + if ($idx != 0) { + $in .= ","; + } + $in .= "?"; + array_push($this->param, $item); + } + $in .= ")"; + return $in; + } + + /// + /// OPERATORS + /// + + public function like(mixed $item): DatabaseQuery + { + $this->query .= "LIKE ?\n"; + array_push($this->param, $item); + return $this; + } + + public function eq(mixed $item): DatabaseQuery + { + $this->query .= "= ?\n"; + array_push($this->param, $item); + return $this; + } + + public function ne(mixed $item): DatabaseQuery + { + $this->query .= "<> ?\n"; + array_push($this->param, $item); + return $this; + } + + public function lt(mixed $item): DatabaseQuery + { + $this->query .= "< ?\n"; + array_push($this->param, $item); + return $this; + } + + public function le(mixed $item): DatabaseQuery + { + $this->query .= "<= ?\n"; + array_push($this->param, $item); + return $this; + } + + /// + /// JOINS + /// + + public function join(string $table, string $on, string $type = 'LEFT'): DatabaseQuery + { + $this->query .= "$type JOIN $table ON $on\n"; + return $this; + } + + /// + /// LIMIT, OFFSET, ORDER + /// + + public function limit(int $limit): DatabaseQuery + { + $this->query .= "LIMIT ?\n"; + array_push($this->param, $limit); + return $this; + } + + public function offset(int $offset): DatabaseQuery + { + $this->query .= "OFFSET ?\n"; + array_push($this->param, $offset); + return $this; + } + + public function order_by(string $column, string $order = 'ASC'): DatabaseQuery + { + $this->query .= "ORDER BY " . $column . ' ' . $order . ' '; + return $this; + } + + /// + /// COLLECT + /// + + public function rows(mixed ...$params): array + { + $args = $this->param; + foreach ($params as $param) { + array_push($args, $param); + } + $stmt = $this->conn->prepare($this->query); + try { + $stmt->execute($args); + } catch (Exception $ex) { + echo $ex; + echo '<br> >> caused by <<<br>'; + echo str_replace("\n", "<br>", $this->query); + } + return __nullify($stmt->fetchAll(PDO::FETCH_ASSOC)) ?? []; + } + + public function row(mixed ...$params): ?array + { + $args = $this->param; + foreach ($params as $param) { + array_push($args, $param); + } + $stmt = $this->conn->prepare($this->query); + $stmt->execute($args); + return __nullify($stmt->fetch(PDO::FETCH_ASSOC)); + } + + public function execute(mixed ...$params): bool + { + $args = $this->param; + foreach ($params as $param) { + array_push($args, $param); + } + $stmt = $this->conn->prepare($this->query); + try { + $stmt->execute($args); + return TRUE; + } catch (Exception $_e) { + echo $_e; + echo '<br> >> caused by <<<br>'; + echo str_replace("\n", "<br>", $this->query); + return FALSE; + } + } +} + +/** + * DatabaseHelper + * allows queries on the + * postgres database + */ +class DatabaseHelper { + + private ?\PDO $conn; + + function __construct() + { + $this->conn = NULL; + } + + private function connect(): \PDO + { + if ($this->conn === NULL) { + $user = getenv("POSTGRES_USER"); + $pass = getenv("POSTGRES_PASSWORD"); + $db = getenv("POSTGRES_DB"); + $host = 'postgres'; + $port = '5432'; + + $conn_str = sprintf("pgsql:host=%s;port=%d;dbname=%s;user=%s;password=%s", + $host, + $port, + $db, + $user, + $pass + ); + $this->conn = new \PDO($conn_str); + $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + return $this->conn; + } + + public function select(string $select): DatabaseQuery + { + $conn = $this->connect(); + $query = new DatabaseQuery($conn); + return $query->select($select); + } + + public function insert_into(string $insert, string ...$columns): DatabaseQuery + { + $conn = $this->connect(); + $query = new DatabaseQuery($conn); + return $query->insert_into($insert, ...$columns); + } + + public function query(string $query_str): DatabaseQuery + { + $conn = $this->connect(); + $query = new DatabaseQuery($conn); + return $query->query($query_str); + } +} + diff --git a/src/lib/error.php b/src/lib/error.php new file mode 100644 index 0000000..ae772d2 --- /dev/null +++ b/src/lib/error.php @@ -0,0 +1,218 @@ +<?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/>. + +/** + * CRIMSON ERROR FUNCTIONS + * + * Insead of using trigger_error, crimson has its own CRIMSON_<type> error + * functions. It is prefered to use this they hook more nicely into crimson. + * trigger_error is still okay to use since crimson creates its own error + * handler, and will catch it anyways. + * + * WARNING: DO NOT create your own error handler since this MAY interfere with + * crimson's control flow. + */ + +// crimson's fatal error handler will set $errno to CRIMSON_E_FATAL_ERROR. +define('CRIMSON_E_FATAL_ERROR', 0); + +function __CRIMSON_error_pretty_print_name(int $errno) { + switch ($errno) { + case CRIMSON_E_FATAL_ERROR: // 0 // + return 'Fatal Error'; + + case E_ERROR: // 1 // + case E_CORE_ERROR: // 16 // + case E_USER_ERROR: // 256 // + case E_RECOVERABLE_ERROR: // 4096 // + return 'Error'; + + case E_WARNING: // 2 // + case E_CORE_WARNING: // 32 // + case E_USER_WARNING: // 512 // + return 'Warning'; + + case E_PARSE: // 4 // + return 'Parse Error'; + + case E_NOTICE: // 8 // + case E_USER_NOTICE: // 1024 // + return 'Notice'; + + case E_COMPILE_ERROR: // 128 // + return 'Compile Error'; + + case E_DEPRECATED: // 8192 // + case E_USER_DEPRECATED: // 16384 // + return 'Deprecated'; + + default: + return 'Unknown Error'; + } +} + +function __CRIMSON_error_pretty_print_bg_color(int $errno) { + switch ($errno) { + case CRIMSON_E_FATAL_ERROR: // 0 // + case E_ERROR: // 1 // + case E_PARSE: // 4 // + case E_CORE_ERROR: // 16 // + case E_COMPILE_ERROR: // 128 // + case E_USER_ERROR: // 256 // + case E_RECOVERABLE_ERROR: // 4096 // + return '#dc143c'; + + case E_WARNING: // 2 // + case E_CORE_WARNING: // 32 // + case E_USER_WARNING: // 512 // + case E_DEPRECATED: // 8192 // + case E_USER_DEPRECATED: // 16384 // + return '#db6d13'; + + case E_NOTICE: // 8 // + case E_USER_NOTICE: // 1024 // + default: + return '#13a6db'; + } +} + +function __CRIMSON_error_pretty_print_text_color(int $errno) { + switch ($errno) { + case CRIMSON_E_FATAL_ERROR: // 0 // + case E_ERROR: // 1 // + case E_PARSE: // 4 // + case E_CORE_ERROR: // 16 // + case E_COMPILE_ERROR: // 128 // + case E_USER_ERROR: // 256 // + case E_RECOVERABLE_ERROR: // 4096 // + return '#fff'; + + case E_WARNING: // 2 // + case E_CORE_WARNING: // 32 // + case E_USER_WARNING: // 512 // + case E_DEPRECATED: // 8192 // + case E_USER_DEPRECATED: // 16384 // + return '#fff'; + + case E_NOTICE: // 8 // + case E_USER_NOTICE: // 1024 // + default: + return '#181818'; + } +} + +/** + * __CRIMSON_error_pretty_print + * + * Prints a pretty HTML error message using the same set of parameters + * from PHP's error handler. + * + * Unless CRIMSON detects that you are using a tty, crimson will opt to + * pretty print all errors. + */ +function __CRIMSON_error_pretty_print( + int $errno, + string $errstr, + ?string $errfile = NULL, + ?int $errline = NULL, + ?array $errcontext = NULL, +): void { + + $name = __CRIMSON_error_pretty_print_name($errno); + $bg = __CRIMSON_error_pretty_print_bg_color($errno); + $text = __CRIMSON_error_pretty_print_text_color($errno); + + $root_style = " + all: unset !important; + display: block !important; + margin: .1em !important; + background: {$bg} !important; + color: {$text} !important; + font-family: Helvetica, Verdana, Courier, monospace !important; + font-size: 14px !important;"; + + $div_style = " + padding: .5em; !important;"; + + $span_style = " + display: block !important"; + + $title_style=" + font-size: 1.2em !important; + font-weight: bold !important; + background: rgba(255,255,255,.2); !important"; + + $html = <<<EOF +<div style="{$root_style}"> + <div style="{$div_style}\n{$title_style}"> + (!) {$name} + </div> + <div style="{$div_style}"> + <span style="{$span_style}"> + In file {$errfile}:{$errline} + </span> + <span style="{$span_style}"> + >>> {$errstr} + </span> + </div> +</div> +EOF; + echo $html; +} + +function __CRIMSON_error_print( + int $errno, + string $errstr, + ?string $errfile = NULL, + ?int $errline = NULL, + ?array $errcontext = NULL, +): void { + $name = __CRIMSON_error_pretty_print_name($errno); + echo "{$name}: {$errstr}\n\tIn file {$errfile}:{$errline}\n"; +} + +function CRIMSON_error_handler( + int $errno, + string $errstr, + ?string $errfile = NULL, + ?int $errline = NULL, + ?array $errcontext = NULL, +): void { + __CRIMSON_error_pretty_print($errno, $errstr, $errfile, $errline, $errcontext); +} + +set_error_handler("CRIMSON_error_handler"); + +function CRIMSON_ERROR(string $msg): void { + $frame = debug_backtrace()[1]; + CRIMSON_error_handler(E_ERROR, $msg, + $frame['file'], $frame['line']); +} + +function CRIMSON_FATAL_ERROR(string $msg): void { + $frame = debug_backtrace()[1]; + CRIMSON_error_handler(CRIMSON_E_FATAL_ERROR, $msg, + $frame['file'], $frame['line']); +} + +function CRIMSON_WARNING(string $msg): void { + $frame = debug_backtrace()[1]; + CRIMSON_error_handler(E_WARNING, $msg, + $frame['file'], $frame['line']); +} diff --git a/src/lib/hooks.php b/src/lib/hooks.php new file mode 100644 index 0000000..21854bd --- /dev/null +++ b/src/lib/hooks.php @@ -0,0 +1,178 @@ +<?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/>. + +/** + * CRIMSON HOOKS + * + * Crimson supports hooks to allow the user to add functionality inside + * crimson's control flow. This can be used for handling errors, or having + * custom routing functionality. + * + * The current supported hooks are: 'error', 'pre_route', 'init', and 'die'. + * + * To run a hook yourself you can call CRIMSON_HOOK($name, [$args]); + * + * To create a handler, make a function in the global scope called: + * CRIMSON_<name>_hook. i.e. if you are making a init hook handler, make a + * function called CRIMSON_init_hook(...). + * + * If a hook is called and a user hook handeler does not exist, the crimson + * builtin hook will be run instead. + * + * NOTE: It is also allowed for you to create your own hook types, but you must + * also create your own handler for it. If you dont, it will result in a error + * since crimson will not have a builtin hook handler to fall back to. + * + * NOTE: CRIMSON_HOOK does support a third argument called $builtin, but this + * is only to be used by crimson itself, so please dont use it. + * + * WARNING: If a hook cannot find a handler to run, it will call + * CRIMSON_FATAL_ERROR and NEVER return. + */ + +/** + * CRIMSON builtin ERROR hook. + * + * $req - The current request parsed by the Router. All fields are gurenteed + * to be set BESIDES 'uri' and 'uri_str', since parsing of the URI can fail + * and result to a call to this error handler. + * + * This hook is run when any Controller or the Router runs into an issue and + * fails with an HTTP Error. + * + * EXAMPLE: If the Router cannot find the requested Controller and Method + * based in the URI path, it will raise 404 Not Found. + * + * EXAMPLE: If the crimson postgres database is enabled, and db-init either + * failed or is still running (the database is not ready), crimson will raise + * 503 Service Unavaliable. + * + * EXAMPLE: If the provided URI to PHP is unable to be parsed, crimson will + * raise 400 Bad Request. + * + * This is hook is called in ROUTER->handle_error(int code) and + * Controller->error(int code). + * + * NOTE: Unlike CRIMSON's DIE hook, it is supported to re-enter crimson code. + * This error handler must never return, but it is okay to recall crimson to + * try to print an error page. + * + * WARNING: If the user tries to re-enter crimson from this handler and ends + * up raising another error, the user error handler WILL NOT be called again. + * To prevent recursion, a user error handle WILL only ever be called ONCE. + * This is due to that the handler has return type 'never', and is also not + * allowed to recurse. + */ +function CRIMSON_builtin_error_hook(array $req, int $code): never +{ + http_response_code($code); + CRIMSON_DIE("{$code} {$req['uri_str']}"); +} + +/** + * CRIMSON builtin PRE ROUTE hook. + * + * This hook does nothing by default since all required CRIMSON routing is done + * in router.php. + */ +function CRIMSON_builtin_pre_route_hook(Router $router): void +{ +} + +/** + * CRIMSON builtin INIT hook. + * + * This hook does nothing by default since all required CRIMSON init work is + * run in index.php. + * + * This hook can be overridden to run any user required initalization code + * before CRIMSON launches and runs the router. + * + * WARNING: The ROUTER is NOT YET created when this hook is run. Do not call + * ROUTER or ANY CRIMSON code in a user specified init hook. Doing so is + * UNDEFINED BEHAVIOR. + */ +function CRIMSON_builtin_init_hook(): void +{ +} + +/** + * CRIMSON builtin DIE hook. + * + * Calls die with $status, i.e. this is an alias for die(). + */ +function CRIMSON_builtin_die_hook(string|int $status = 0): never +{ + die($status); +} + +/** + * Executes a hook for crimson. + * + * $name - the name of the hook + * $args - the arguments to pass to the hook + * $builtin - force the use of the builtin hook + * + * Looks for CRIMSON_$name_hook first, which is the custom user defined hook. + * If it does not exist, it will run CRIMSON_builtin_$name_hook instead. + */ +function CRIMSON_HOOK( + string $name, + array $args = array(), + bool $builtin = FALSE, +): void { + $names = array(); + if (!$builtin) + $names[] = "CRIMSON_{$name}_hook"; + $names[] = "CRIMSON_builtin_{$name}_hook"; + + // find handler + $handler = NULL; + foreach ($names as $name) { + try { + $handler = new ReflectionFunction($name); + if ($handler) + break; + } catch (Exception $_e) {}; + } + + // error in invalid hook + if (!$handler) { + CRIMSON_ERROR("Invalid hook: {$name}"); + return; + } + + // i dont care to argument check, + // if someone screws up a hook we have bigger problems + $handler->invoke(...$args); +} + +/** + * Executes die() in php. But allows the user to add a hook to handle any + * loose resources before php kills itself. + * + * NOTE: A DIE hook should NEVER be handeled and return. A user provided hook + * must also have a return type of never and immediately die. Do NOT + * call any crimson code such as rerunning the ROUTER. Doing so will result in + * UNDEFINED BEHAVIOR and nasal demons showing up at your doorstep!!! + */ +function CRIMSON_DIE(string|int $status = 0): never +{ + CRIMSON_HOOK('die', [$status]); +} diff --git a/src/lib/html.php b/src/lib/html.php new file mode 100644 index 0000000..fac7a94 --- /dev/null +++ b/src/lib/html.php @@ -0,0 +1,95 @@ +<?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/>. + +function esc(string $data, bool $string_esc = FALSE): string { + $flags = ENT_SUBSTITUTE | ENT_HTML401; + if ($string_esc) + $flags |= ENT_QUOTES; + return htmlspecialchars($data, $flags); +} + +function status_code_msg(int $code): ?string { + + static $status_code = array( + 100 => "Continue", + 101 => "Switching Protocols", + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non-Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Found", + 303 => "See Other", + 304 => "Not Modified", + //305 => "Use Proxy", + //306 => "unused", + 307 => "Temporary Redirect", + 308 => "Permanent Redirect", + 400 => "Bad Request", + 401 => "Unauthorized", + 402 => "Payment Required", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 407 => "Proxy Authentication Required", + 408 => "Request Timeout", + 409 => "Conflict", + 410 => "Gone", + 411 => "Length Required", + 412 => "Precondition Failed", + 413 => "Content Too Large", + 414 => "URI Too Long", + 415 => "Unsupported Media Type", + 416 => "Range Not Satisfiable", + 417 => "Expectation Failed", + 418 => "I'm a teapot", + 421 => "Misdirected Request", + 422 => "Unprocessable Content", + 423 => "Locked", + 424 => "Failed Dependency", + 425 => "Too Early", + 426 => "Upgrade Required", + 428 => "Precondition Required", + 429 => "Too Many Requests", + 431 => "Request Header Fields Too Large", + 451 => "Unavailable For Legal Reasons", + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 505 => "HTTP Version Not Supported", + 506 => "Variant Also Negotiates", + 507 => "Insufficient Storage", + 508 => "Loop Detected", + 510 => "Not Extended", + 511 => "Network Authentication Required", + ); + + return $status_code[$code] ?? NULL; +} + +function is_valid_status_code(int $code): bool { + return is_string(status_code_msg($code)); +} diff --git a/src/lib/lang.php b/src/lib/lang.php new file mode 100644 index 0000000..84a4215 --- /dev/null +++ b/src/lib/lang.php @@ -0,0 +1,186 @@ +<?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/>. + +/** + * Returns the lang string for the provided $key + * + * $key - The key of the lang string + * $default - The string to use if lang string $key is undefined + * $sub - List of values to substitute using php's sprinf + */ +function lang( + string $key, + ?string $default = NULL, + ?array $sub = NULL +): string { + $lang = ROUTER->get_lang(); + $result = NULL; + + // lookup lang key + if (isset($lang[$key])) + $result = $lang[$key]; + + // replace with $default if undefined + if ($result === NULL && $default !== NULL) + $result = $default; + + // error if undefined + if ($result === NULL) { + CRIMSON_WARNING('Undefined lang string: ' . $key); + return $key; + } + + // make substitutions + if ($sub) { + if (!is_array($sub)) + $sub = [$sub]; + $result = sprintf($result, ...$sub); + } + + return $result; +} + +/** + * Returns a html element (button, a, div, ...) containing content from + * the lang string, icon, aria tags, and/or tooltips. Text content and icon + * are contained in a seconed inner html element (span, h1, ...). + * + * ilang has up to four parts: text, tooltip, icon class, and icon content. + * Each part is loaded from a different lang string using $key as the prefix. + * + * == LANG_KEYS == + * + * NAME | LANG KEY | REQUIRED | DESCRIPTION + * -------------------------------------------------------------------------- + * text | $key_text | yes | The text content of the element. Text + * | | | content will always uppercase the first + * | | | letter. + * tip | $key_tip | no | The tool tip of the element. + * icon | $key_icon | no | Adds a <i> element with the class + * | | | <lang string>. + * content | $key_content | no | If icon, adds <lang string> as the + * | | | inner html of the icon. + * + * == ARGUMENTS == + * + * NAME | REQUIRED | DEFAULT | DESCRIPTION + * --------------------------------------------------------------------------- + * $key | yes | | The key of the interface lang string. + * $class | no | | The class of the html element. + * $id | no | | The id of the html element. + * $sub | no | [] | Substitution arguments passed into lang() + * | | | in both $key_text and $key_tip. + * $type | no | 'a' | Sets the type of the html element. + * $subtype | no | 'span' | Sets the type of the inner html element. + * $attrs | no | array() | Sets html attributes using the key/value + * | | | pairs from $attrs. $class, $id, $href, and + * | | | and $onclick are all short hand for + * | | | $attrs['<name>']; Named attr arguments take + * | | | priority over any defined in $attrs. + * $style | no | | $attrs['style'] = $style. + * $href | no | | $attrs['href'] = $href. $type = 'a'; + * $onclick | no | | $attrs['onclick'] = $onclick. $type = 'button'. + * + * NOTE: For any non required argument that is falsy, it is converted back to + * its default value. + * + * NOTE: For any non required argument that does not have a default value + * listed, falsy values turn off that attribute or functionality. + * + * WARNING: $href and $onclick also modify the default $type. If $type is + * passed to ilang, that type will be used instead. + * + * WARNING: Lang strings WILL be html escaped along with each atribute value. + * Everything else will not be sanitized by this function. + */ +function ilang( + string $key, + ?string $class = NULL, + ?string $id = NULL, + array $sub = [], + ?string $type = NULL, + string $subtype = 'span', + array $attrs = array(), + ?string $style = NULL, + ?string $href = NULL, + ?string $onclick = NULL, +): string { + // read lang keys + $text = lang("{$key}_text", sub: $sub); + $tip = lang("{$key}_tip", '', sub: $sub); + $icon = lang("{$key}_icon", ''); + $content = lang("{$key}_content", ''); + + // uppercase + $text = ucfirst($text); + + // set $type if falsy + if (!$type) { + if ($href) + $type = 'a'; + else if ($onclick) + $type = 'button'; + else + $type = 'a'; + } + + // populate $attrs with named arguments + if ($tip) { + $attrs['title'] = $tip; + $attrs['aria-label'] = $tip; + } + if ($class) + $attrs['class'] = "{$class} ilang"; + else + $attrs['class'] = "ilang"; + if ($id) + $attrs['id'] = $id; + if ($style) + $attrs['style'] = $style; + if ($href) + $attrs['href'] = $href; + if ($onclick) + $attrs['onclick'] = $onclick; + + $html = ""; + // open tag + $html .= "<{$type}"; + foreach ($attrs as $key => $value) { + $value = esc($value, TRUE); // html tag & string escape + $html .= " {$key}=\"{$value}\""; + } + $html .= ">"; + // icon + if ($icon) { + $icon = esc($icon, TRUE); // html tag & string escape + $html .= "<i class=\"{$icon}\">"; + if ($content) { + $content = esc($content); // html tag escape + $html .= "{$content}"; + } + $html .= "</i>"; + } + // content + $text = esc($text); // html tag escape + $html .= "<{$subtype}>{$text}</{$subtype}>"; + // close tag + $html .= "</{$type}>"; + + return $html; +} diff --git a/src/lib/meta.php b/src/lib/meta.php new file mode 100644 index 0000000..471d3e3 --- /dev/null +++ b/src/lib/meta.php @@ -0,0 +1,28 @@ +<?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/>. + +function CRIMSON_META(Controller $self): string { + $data = $self->CRIMSON_data; + $html = ''; + foreach ($data['css'] as $css) + $html .= $self->link_css($css); + foreach ($data['js'] as $js) + $html .= $self->link_js($js); + return $html; +} 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(); + } + +} |