summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2024-12-23 10:39:16 -0500
committerFreya Murphy <freya@freyacat.org>2024-12-23 10:39:16 -0500
commitde9cae795f93d03e68d965c59af4b21d96df4ec7 (patch)
treead4f903c04630b3b92e2b9b5d06d5b8647d299bb /src/lib
parentlicense (diff)
downloadcrimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.tar.gz
crimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.tar.bz2
crimson-de9cae795f93d03e68d965c59af4b21d96df4ec7.zip
initial
Diffstat (limited to 'src/lib')
-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
6 files changed, 1042 insertions, 0 deletions
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;
+}