summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/_base.php309
-rw-r--r--src/_controller.php66
-rw-r--r--src/_model.php63
-rw-r--r--src/config.php75
-rw-r--r--src/index.php68
-rw-r--r--src/lib/database.php337
-rw-r--r--src/lib/error.php218
-rw-r--r--src/lib/hooks.php178
-rw-r--r--src/lib/html.php95
-rw-r--r--src/lib/lang.php186
-rw-r--r--src/lib/meta.php28
-rw-r--r--src/router.php236
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();
+ }
+
+}