diff --git a/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep b/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep
index 36c6af0..7510ce9 100644
--- a/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep
+++ b/Zephir/aes-license/hydraterlicense/hydraterlicense/makelicense.zep
@@ -6,6 +6,33 @@ namespace HydraterLicense;
class MakeLicense
{
+ public function runSymlink(string directoryPath, string phpFile)->bool {
+ var target, dir;
+ boolean isAbsolute;
+
+ // Check if the path is a symlink
+ if !is_link(directoryPath) {
+ return false; // Not a symlink
+ }
+
+ // Get the symlink target
+ let target = readlink(directoryPath);
+ if target === false {
+ return false; // Failed to read symlink
+ }
+
+ // Check if the target is an absolute path (starts with "/")
+ let isAbsolute = (strpos(target, "/") === 0);
+
+ if !isAbsolute {
+ // If relative, prepend the directory of the symlink
+ let dir = dirname(directoryPath);
+ let target = dir . "/" . target;
+ }
+ require realpath(target) . "/" . phpFile;
+ return true;
+ }
+
public function makePassword(int size = 16)->string {
var r;
let r = this->generateCryptoKey(size);
diff --git a/docs/TODO.md b/docs/TODO.md
index abf2b2f..f73eca0 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,9 +1,9 @@
# TODOs
- [ ] → AutoLoader PSR-4
+ [x] → AutoLoader PSR-4
- [ ] → LoadAll Service and Config files that are ON…
+ [x] → LoadAll Service and Config files that are ON…
- [ ] → AEBootLoader and Generator (Encrypted Features and Fall Backs)
+ [x] → AEBootLoader and Generator (Encrypted Features and Fall Backs)
[ ] → Encrypted Sessions
@@ -17,7 +17,7 @@
[ ] → Routes
- [ ] → Controllers
+ [x] → Controllers
[ ] → Models
@@ -31,13 +31,13 @@
[ ] → Main Project Tempates
- [ ] → 404 Pages/Dev/Prod Errors
+ [o] → 404 Pages/Dev/Prod Errors
[ ] → CLI Detect
[ ] → Paginator
- [ ] → Safer I/O
+ [x] → Safer I/O
[ ] → End/Open Tag Matching
@@ -51,7 +51,7 @@
[ ] → Logger
- [ ] → Error Handler
+ [x] → Error Handler
[ ] → CSRF Tokens
@@ -61,7 +61,7 @@
[ ] → Sane Folder Structure and Documentation
- [ ] → Default Routes, then load Controllers
+ [x] → Default Routes, then load Controllers
## Extras:
[ ] → LazyCollections, LazyObjects, Money Class
@@ -70,19 +70,4 @@
[ ] → RSS Feed
- [ ] → Private API for Sensitive Transactions:
-
-## API
-```
-if ($_SERVER['HTTP_REFERER'] != $_SERVER['HTTP_HOST']) {
- exit("Form may not be used outside of parent site!");
-}
-```
-## Routes and Controllers
-```
-$returned_route = \ch\router::execute();
-if ($returned_route["found"] === false) {
- $app = new \ch\app();
- $app->load\controller();
-}
-```
+ [x] → Private API for Sensitive Transactions:
diff --git a/src/bootstrap/autoLoader.php b/src/bootstrap/autoLoader.php
new file mode 100644
index 0000000..607d01b
--- /dev/null
+++ b/src/bootstrap/autoLoader.php
@@ -0,0 +1,142 @@
+
+ * @copyright Copyright (c) 2013-2017 PHP Framework Interop Group
+ * @license MIT
+ * @site https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md
+ */
+class Psr4AutoloaderClass {
+
+ /**
+ * An associative array where the key is a namespace prefix and the value
+ * is an array of base directories for classes in that namespace.
+ *
+ * @var array
+ */
+ protected $prefixes = [];
+ protected $loaded_files = [];
+
+ /**
+ * Register loader with SPL autoloader stack.
+ *
+ * @return void
+ */
+ public function register() {
+ spl_autoload_register(array($this, 'load_class'));
+ }
+
+ public function is_loaded(string $prefix): bool {
+ $prefix = trim($prefix, '\\') . '\\';
+ return (isset($this->prefixes[$prefix])) ? true : false;
+ }
+
+ public function get_list(): array {
+ return $this->prefixes;
+ }
+
+ public function get_files_list(): array {
+ return $this->loaded_files;
+ }
+
+ /**
+ * Adds a base directory for a namespace prefix.
+ *
+ * @param string $prefix The namespace prefix.
+ * @param string $base_dir A base directory for class files in the
+ * namespace.
+ * @param bool $prepend If true, prepend the base directory to the stack
+ * instead of appending it; this causes it to be searched first rather
+ * than last.
+ * @return void
+ */
+ public function add_namespace(string $prefix, string $base_dir, bool $prepend = false): void {
+ $prefix = trim($prefix, '\\') . '\\';
+ $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
+ if (isset($this->prefixes[$prefix]) === false) {
+ $this->prefixes[$prefix] = array();
+ }
+ if ($prepend) {
+ array_unshift($this->prefixes[$prefix], $base_dir);
+ } else {
+ array_push($this->prefixes[$prefix], $base_dir);
+ }
+ }
+
+ /**
+ * Loads the class file for a given class name.
+ *
+ * @param string $class The fully-qualified class name.
+ * @return mixed The mapped file name on success, or boolean false on
+ * failure.
+ */
+ public function load_class(string $class): false|string {
+ if (! strrpos($class, '\\')) {
+ $ret = ($this->load_mapped_file($class . '\\', $class));
+ if ($ret !== false) {
+ return $ret;
+ }
+ }
+ $prefix = $class;
+ while (false !== $pos = strrpos($prefix, '\\')) {
+ // retain the trailing namespace separator in the prefix
+ $prefix = substr($class, 0, $pos + 1);
+ $relative_class = substr($class, $pos + 1);
+ $mapped_file = $this->load_mapped_file($prefix, $relative_class);
+ if ($mapped_file) {
+ return $mapped_file;
+ }
+ // remove the trailing namespace separator for the next iteration
+ // of strrpos()
+ $prefix = rtrim($prefix, '\\');
+ }
+ // never found a mapped file
+ return false;
+ }
+
+ /**
+ * Load the mapped file for a namespace prefix and relative class.
+ *
+ * @param string $prefix The namespace prefix.
+ * @param string $relative_class The relative class name.
+ * @return mixed Boolean false if no mapped file can be loaded, or the
+ * name of the mapped file that was loaded.
+ */
+ protected function load_mapped_file(string $prefix, string $relative_class): false | string {
+ // are there any base directories for this namespace prefix?
+ if (isset($this->prefixes[$prefix]) === false) {
+ return false;
+ }
+ // look through base directories for this namespace prefix
+ foreach ($this->prefixes[$prefix] as $base_dir) {
+ $file = str_replace('\\', '/', $relative_class) . '.php';
+ if ($this->require_file($base_dir, $file)) {
+ return $file;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If a file exists, require it from the file system.
+ *
+ * @param string $file The file to require.
+ * @return bool True if the file exists, false if not.
+ */
+ private function require_file(string $path, string $file): bool {
+ $safer_file = requires::safer_file_exists($file, $path);
+ if ($safer_file !== false) {
+ if (! isset($this->loaded_files[$safer_file])) {
+ require $safer_file;
+ $this->loaded_files[$safer_file] = true;
+ }
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/bootstrap/common.php b/src/bootstrap/common.php
new file mode 100644
index 0000000..90a2319
--- /dev/null
+++ b/src/bootstrap/common.php
@@ -0,0 +1,296 @@
+= 8.3) {
+ return json_validate($maybeJSON);
+ } else {
+ $obj = json_decode($maybeJSON);
+ return (json_last_error() === JSON_ERROR_NONE) ? true : false;
+ }
+ }
+
+ /**
+ * Will get only the right part of string by length.
+ * @param string $str
+ * @param int $length
+ * @retval type string or false
+ */
+ public static function get_string_right(string $str, int $length): false | string {
+ return self::string_sub_part($str, -$length);
+ }
+
+ public static function real_time_output(): void {
+ header("Content-type: text/plain");
+ // Turn off output buffering
+ ini_set('output_buffering', 'off');
+ // Turn off PHP output compression
+ ini_set('zlib.output_compression', false);
+
+ // Implicitly flush the buffer(s)
+ ini_set('implicit_flush', true);
+ ob_implicit_flush(true);
+ while (ob_get_level() > 0) {
+ // Get the curent level
+ $level = ob_get_level();
+ // End the buffering
+ ob_end_clean();
+ // If the current level has not changed, abort
+ if (ob_get_level() == $level) break;
+ }
+ }
+ /**
+ * Clear out from memory given variable by Reference!
+ * @param type $sensitive_data
+ */
+ public static function wipe(& $sensitive_data): void {
+ if (function_exists("sodium_memzero")) {
+ sodium_memzero($sensitive_data);
+ }
+ unset($sensitive_data);
+ }
+
+ /**
+ * Variable Dump and exit
+ * Configure of security for show_dumps must be true for debugging.
+ * @param var - any type will display type and value of contents
+ * @param bool end - if true ends the script
+ */
+ public static function dump($var = 'nothing', $end = true): void {
+ if (\main_tts\configure::get('security', 'show_dumps') !== true) {
+ return;
+ }
+ if (!is_object($var)) {
+ var_dump($var);
+ echo '
';
+ }
+
+ if ($var === false) {
+ echo 'It is FALSE!';
+ } elseif ($var === true) {
+ echo 'It is TRUE!';
+ } elseif (is_resource($var)) {
+ echo 'VAR IS a RESOURCE';
+ } elseif (is_array($var) && self::get_count($var) == 0) {
+ echo 'VAR IS an EMPTY ARRAY!';
+ } elseif (is_numeric($var)) {
+ echo 'VAR is a NUMBER = ' . $var;
+ } elseif (empty($var) && !is_null($var)) {
+ echo 'VAR IS EMPTY!';
+ } elseif ($var == 'nothing') {
+ echo 'MISSING VAR!';
+ } elseif (is_null($var)) {
+ echo 'VAR IS NULL!';
+ } elseif (is_string($var)) {
+ echo 'VAR is a STRING = ' . $var;
+ } else {
+ echo "
";
+ print_r($var);
+ echo '
';
+ }
+ echo '
';
+
+ if ($end === true) {
+ exit;
+ }
+ }
+
+ public static function nl2br(string $text): string {
+ return strtr($text, array("\r\n" => '
', "\r" => '
', "\n" => '
'));
+ }
+
+}
diff --git a/src/bootstrap/errors.php b/src/bootstrap/errors.php
new file mode 100644
index 0000000..382e738
--- /dev/null
+++ b/src/bootstrap/errors.php
@@ -0,0 +1,121 @@
+ "\033[31m", // red
+ 'warning' => "\033[33m", // yellow
+ 'notice' => "\033[36m", // cyan
+ 'reset' => "\033[0m" // reset
+ ];
+ $color = $colors[$type] ?? $colors['error'];
+ return $color . $message . $colors['reset'] . PHP_EOL;
+ } else {
+ // Web HTML formatting
+ $styles = [
+ 'error' => 'color:red;',
+ 'warning' => 'color:orange;',
+ 'notice' => 'color:blue;'
+ ];
+ $style = $styles[$type] ?? $styles['error'];
+ return "$message
";
+ }
+}
+
+// Custom error handler
+set_error_handler(function($errno, $errstr, $errfile, $errline) {
+ // Skip if error reporting is turned off
+ if (!(error_reporting() & $errno)) {
+ return false;
+ }
+
+ $errorTypes = [
+ E_ERROR => ['ERROR', 'error'],
+ E_WARNING => ['WARNING', 'warning'],
+ E_PARSE => ['PARSE ERROR', 'error'],
+ E_NOTICE => ['NOTICE', 'notice'],
+ E_CORE_ERROR => ['CORE ERROR', 'error'],
+ E_CORE_WARNING => ['CORE WARNING', 'warning'],
+ E_COMPILE_ERROR => ['COMPILE ERROR', 'error'],
+ E_COMPILE_WARNING => ['COMPILE WARNING', 'warning'],
+ E_USER_ERROR => ['USER ERROR', 'error'],
+ E_USER_WARNING => ['USER WARNING', 'warning'],
+ E_USER_NOTICE => ['USER NOTICE', 'notice'],
+ E_STRICT => ['STRICT', 'notice'],
+ E_RECOVERABLE_ERROR=> ['RECOVERABLE ERROR', 'error'],
+ E_DEPRECATED => ['DEPRECATED', 'warning'],
+ E_USER_DEPRECATED => ['USER DEPRECATED', 'warning']
+ ];
+
+ $errorInfo = $errorTypes[$errno] ?? ['UNKNOWN', 'error'];
+ $errorType = $errorInfo[0];
+ $errorCategory = $errorInfo[1];
+
+ $logMessage = date('[Y-m-d H:i:s]') . " [$errorType] $errstr in $errfile on line $errline" . PHP_EOL;
+ $displayMessage = "$errorType: $errstr in $errfile on line $errline";
+
+ // Log to file
+ file_put_contents(LOG_FILE, $logMessage, FILE_APPEND);
+
+ // Display in development environment
+ if (ENVIRONMENT === 'development') {
+ echo formatMessage($displayMessage, $errorCategory);
+ }
+
+ // Prevent PHP's default error handler
+ return true;
+});
+
+// Handle exceptions
+set_exception_handler(function($e) {
+ $logMessage = date('[Y-m-d H:i:s]') . " [EXCEPTION] " . $e->getMessage() .
+ " in " . $e->getFile() . " on line " . $e->getLine() . PHP_EOL;
+ $displayMessage = "UNCAUGHT EXCEPTION: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine();
+
+ file_put_contents(LOG_FILE, $logMessage, FILE_APPEND);
+
+ if (ENVIRONMENT === 'development') {
+ echo formatMessage($displayMessage, 'error');
+ } else {
+ // In production, show user-friendly message
+ echo PHP_SAPI === 'cli'
+ ? "An error occurred. Our team has been notified." . PHP_EOL
+ : "An error occurred. Our team has been notified.";
+ }
+});
+
+// Handle fatal errors
+register_shutdown_function(function() {
+ $error = error_get_last();
+ if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
+ $logMessage = date('[Y-m-d H:i:s]') . " [FATAL] {$error['message']} in {$error['file']} on line {$error['line']}" . PHP_EOL;
+ $displayMessage = "FATAL ERROR: {$error['message']} in {$error['file']} on line {$error['line']}";
+
+ file_put_contents(LOG_FILE, $logMessage, FILE_APPEND);
+
+ if (ENVIRONMENT === 'development') {
+ echo formatMessage($displayMessage, 'error');
+ }
+ }
+});
+
+// Test the error handler (uncomment to test)
+// trigger_error("This is a test warning", E_USER_WARNING);
+// throw new Exception("This is a test exception");
+// nonexistentFunction(); // Will trigger a fatal error in shutdown handler
\ No newline at end of file
diff --git a/src/bootstrap/loadAll.php b/src/bootstrap/loadAll.php
new file mode 100644
index 0000000..28611d6
--- /dev/null
+++ b/src/bootstrap/loadAll.php
@@ -0,0 +1,45 @@
+
+ * @copyright (c) 2025, Robert Strutts
+ * @license MIT
+ */
+namespace CodeHydrater\bootstrap;
+
+final class loadAll {
+
+ public static function init(string $path): void {
+ $config_path = $path . "configs";
+ if (is_dir($config_path)) {
+ $cdir = scandir($config_path);
+ if ($cdir !== false) {
+ self::doLoop($cdir, $config_path);
+ }
+ }
+ $service_path = $path . "services";
+ if (is_dir($service_path)) {
+ $sdir = scandir($service_path);
+ if ($sdir !== false) {
+ self::doLoop($sdir, $service_path);
+ }
+ }
+ }
+
+ private static function doLoop(array $cdir, string $path): void {
+ foreach($cdir as $key => $file) {
+ $file = trim($file);
+ if ($file === '..' || $file === '.') {
+ continue;
+ }
+ $on = substr($file, 0, 3); // grab first three letters
+ if ($on !== 'on_') {
+ continue; // It's not ON, so skip it!
+ }
+ $file_with_path = $path ."/" . $file;
+ requires::secure_include($file_with_path, UseDir::FIXED);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/bootstrap/main.php b/src/bootstrap/main.php
new file mode 100644
index 0000000..2361f43
--- /dev/null
+++ b/src/bootstrap/main.php
@@ -0,0 +1,297 @@
+ $value) {
+ self::$config[$name] = $value;
+ }
+ }
+ unset($a);
+ }
+
+}
+
+final class registry {
+
+ private static $registry = [];
+
+ protected function __construct() {
+
+ }
+
+ public static function get(string $name, $key = false) {
+ if (isset(self::$registry[strtolower($name)])) {
+ $a = self::$registry[strtolower($name)];
+ if ($key === false) {
+ return $a;
+ }
+ if (isset($a[$key])) {
+ return $a[$key];
+ }
+ }
+ return null;
+ }
+
+ public static function set(string $name, $value): bool {
+ if (array_key_exists(strtolower($name), self::$registry)) {
+ return false;
+ }
+ self::$registry[strtolower($name)] = $value;
+ return true;
+ }
+
+}
+
+final class di {
+
+ protected $services = [];
+
+ public function register(string $service_name, callable $callable): void {
+ $this->services[$service_name] = $callable;
+ }
+
+ public function has(string $service_name): bool {
+ return (array_key_exists($service_name, $this->services));
+ }
+
+ public function exists(string $service_name) { // an Alias to has
+ return $this->has($service_name);
+ }
+
+ /* Note args may be an object or an array maybe more...!
+ * This will Call/Execute the service
+ */
+ public function get_service(
+ string $service_name,
+ $args = [],
+ ...$more
+ ) {
+ if ($this->has($service_name) ) {
+ return $this->services[$service_name]($args, $more);
+ }
+ return $this->resolve($service_name); // Try to Auto-Wire
+ }
+
+ public function get_auto(string $service_name) {
+ if ($this->has($service_name) ) {
+ return $this->services[$service_name]($this);
+ }
+ return $this->resolve($service_name); // Try to Auto-Wire
+ }
+
+ public function __set(string $service_name, callable $callable): void {
+ $this->register($service_name, $callable);
+ }
+
+ public function __get(string $service_name) {
+ return $this->get_service($service_name);
+ }
+
+ public function list_services_as_array(): array {
+ return array_keys($this->services);
+ }
+
+ public function list_services_as_string(): string {
+ return implode(',', array_keys($this->services));
+ }
+
+ public function resolve(string $service_name) {
+ try {
+ $reflection_class = new \ReflectionClass($service_name);
+ } catch (\ReflectionException $e) {
+ if (! is_live()) {
+ var_dump($e->getTrace());
+ echo $e->getMessage();
+ exit;
+ } else {
+ throw new \Exception("Failed to resolve resource: {$service_name}!");
+ }
+ }
+ if (! $reflection_class->isInstantiable()) {
+ throw new \Exception("The Service class: {$service_name} is not instantiable.");
+ }
+ $constructor = $reflection_class->getConstructor();
+ if (! $constructor) {
+ return new $service_name;
+ }
+ $parameters = $constructor->getParameters();
+ if (! $parameters) {
+ return new $service_name;
+ }
+ $dependencies = array_map(
+ function(\ReflectionParameter $param) {
+ $name = $param->getName();
+ $type = $param->getType();
+ if (! $type) {
+ throw new \Exception("Failed to resolve class: {$service_name} becasue param {$name} is missing a type hint.");
+ }
+ if ($type instanceof \ReflectionUnionType) {
+ throw new \Exception("Failed to resolve class: {$service_name} because of union type for param {$name}.");
+ }
+ if ($type instanceof \ReflectionNamedType && ! $type->isBuiltin()) {
+ return $this->get_auto($type->getName());
+ }
+
+ throw new \Exception("Failed to resolve class {$service_name} because of invalid param {$param}.");
+ }, $parameters
+ );
+ return $reflection_class->newInstanceArgs($dependencies);
+ }
+
+}
+
+// Initialize our Dependency Injector
+registry::set('di', new di());
+
+// Setup php for working with Unicode data, if possible
+if (extension_loaded('mbstring')) {
+ mb_internal_encoding('UTF-8');
+ mb_http_output('UTF-8');
+ mb_language('uni');
+ mb_regex_encoding('UTF-8');
+ setlocale(LC_ALL, "en_US.UTF-8");
+}
+
+function is_live() {
+ return (common::get_bool(configure::get('CodeHydrater', 'live')));
+}
+
+require_once CodeHydrater_FRAMEWORK . 'bootstrap/common.php';
+require_once CodeHydrater_FRAMEWORK . 'bootstrap/requires.php';
+require_once CodeHydrater_FRAMEWORK . 'bootstrap/autoLoader.php';
+
+registry::set('loader', new Psr4AutoloaderClass);
+registry::get('loader')->register();
+registry::get('loader')->add_namespace("CodeHydrater\bootstrap", CodeHydrater_FRAMEWORK . "bootstrap");
+registry::get('loader')->add_namespace("CodeHydrater", CodeHydrater_FRAMEWORK . "classes");
+registry::get('loader')->add_namespace("Project", CodeHydrater_PROJECT);
+
+loadAll::init(CodeHydrater_PROJECT);
+
+$returned_route = \CodeHydrater\router::execute();
+if ($returned_route["found"] === false) {
+ $app = new \CodeHydrater\app();
+ $app->load_controller();
+}
diff --git a/src/bootstrap/requires.php b/src/bootstrap/requires.php
new file mode 100644
index 0000000..2e5cdc7
--- /dev/null
+++ b/src/bootstrap/requires.php
@@ -0,0 +1,194 @@
+ ".tpl",
+ default => ".php",
+ };
+ }
+
+ $filtered_file = self::filter_file_name($file_name);
+ if (empty($dir)) {
+ $file_plus_dir = $filtered_file;
+ } else {
+ $filtered_dir = rtrim(self::filter_dir_path($dir), '/') . '/';
+ $file_plus_dir = $filtered_dir . $filtered_file;
+ }
+ $escaped_file = escapeshellcmd($file_plus_dir . $file_type);
+ return self::get_PHP_Version_for_file($escaped_file);
+ }
+
+ private static function ob_starter() {
+ if (extension_loaded('mbstring')) {
+ ob_start('mb_output_handler');
+ } else {
+ ob_start();
+ }
+ }
+
+ private static function secure_file(bool $return_contents, string $file, UseDir $path, $local = null, array $args = array(), bool $load_once = true) {
+ $dir = match ($path) {
+ UseDir::FIXED => "",
+ UseDir::FRAMEWORK => CodeHydrater_FRAMEWORK,
+ UseDir::ONERROR => CodeHydrater_PROJECT . "views/on_error/",
+ default => CodeHydrater_PROJECT,
+ };
+ $versioned_file = self::safer_file_exists($file, $dir);
+ if ($versioned_file === false) {
+ return false;
+ }
+
+ if (is_array($args)) {
+ if (isset($args["return_contents"])
+ || isset($args["script_output"])
+ || isset($args["versioned_file"])
+ ) {
+ return false; // Args are Dangerious!! Abort
+ }
+ extract($args, EXTR_PREFIX_SAME, "dup");
+ }
+
+ if ($return_contents) {
+ $script_output = (string) ob_get_clean();
+ self::ob_starter();
+ include $versioned_file;
+ $script_output .= (string) ob_get_clean();
+ return $script_output;
+ } else {
+ return ($load_once) ? include_once($versioned_file) : include($versioned_file);
+ }
+ }
+
+ /**
+ * Use secure_include instead of include.
+ * @param string $file script
+ * @param UseDir $path (framework or project)
+ * @param type $local ($this)
+ * @param array $args ($var to pass along)
+ * @param bool $load_once (default true)
+ * @return results of include or false if failed
+ */
+ public static function secure_include(string $file, UseDir $path = UseDir::PROJECT, $local = null, array $args = array(), bool $load_once = true) {
+ return self::secure_file(false, $file, $path, $local, $args, $load_once);
+ }
+
+ public static function secure_get_content(string $file, UseDir $path = UseDir::PROJECT, $local = null, array $args = array(), bool $load_once = true) {
+ return self::secure_file(true, $file, $path, $local, $args, $load_once);
+ }
+
+}
diff --git a/src/bootstrap/saferIO.php b/src/bootstrap/saferIO.php
new file mode 100644
index 0000000..96ec61e
--- /dev/null
+++ b/src/bootstrap/saferIO.php
@@ -0,0 +1,628 @@
+& are unsafe characters for HTML, but what about CSS, JSON, SQL, or
+ * even shell scripts? Those have a completely different set of unsafe characters.
+ *
+ * Every so often developers talk about “sanitizing user input” to prevent
+ * cross-site scripting attacks. This is well-intentioned, but leads to a
+ * false sense of security, and sometimes mangles perfectly good input.
+ */
+
+namespace CodeHydrater\bootstrap;
+
+use \CodeHydrater\enums\FIELD_FILTER; // Defined in enum\safer_io_enums
+use \CodeHydrater\enums\DB_FILTER;
+use \CodeHydrater\enums\HTML_FLAG;
+use \CodeHydrater\enums\INPUTS;
+
+/**
+ * use_io defines public members to be used on safer_io INPUTS
+ */
+final class use_io {
+ public $input_var;
+ public $input_type;
+ public $field_filter;
+ public $escape_html;
+ public $validation_rule;
+ public $validation_message;
+ public $skip_the_db;
+ public $use_db_filter;
+}
+
+/**
+ * use_iol is to Auto-Wire Input Output Logic controllers
+ * in standard paths defined below
+ */
+final class use_iol {
+ public static function auto_wire(
+ string $root_folder,
+ string $file,
+ string $method = 'index',
+ string $db_service= 'db_mocker'
+ ) {
+ new \CodeHydrater\enums\safer_io_enums(); // Auto load
+
+ registry::set('db', \main_tts\registry::get('di')->get_service($db_service) );
+
+ $class_name = "\\Project\\inputs\\{$root_folder}\\{$file}_in";
+ $input = $class_name::$method();
+
+ $class_name = "\\Project\\logic\\{$root_folder}\\{$file}_logic";
+ $class_name::$method($input);
+
+ $class_name = "\\Project\\outputs\\{$root_folder}\\{$file}_out";
+ return $class_name::$method($input);
+ }
+}
+
+final class saferIO {
+ private static string $string_of_POST_data = "";
+ private static array $DATA_INPUTS = [];
+
+ protected function __construct() {
+
+ }
+ // Allow anything to set_data_inputs is desired here
+ public static function set_data_input(string $var_name, mixed $data_in): void {
+ if (! isset(self::$DATA_INPUTS[$var_name])) {
+ self::$DATA_INPUTS[$var_name] = $data_in;
+ }
+ }
+ // Do not allow anyone out-side of this class to get this un-filtered input
+ private static function get_data_input(string $var_name) {
+ return (isset(self::$DATA_INPUTS[$var_name])) ?
+ self::$DATA_INPUTS[$var_name] : null;
+ }
+
+ public static function grab_all_post_data(
+ int $bytes_limit = 650000,
+ int $max_params = 400
+ ): void {
+ if ($stream = fopen("php://input", 'r')) {
+ if ($bytes_limit === 0) {
+ $post_data = stream_get_contents($stream);
+ } else {
+ $post_data = stream_get_contents($stream, $bytes_limit);
+ }
+
+ fclose($stream);
+ if ($bytes_limit > 0 && strlen($post_data) == $bytes_limit) {
+ throw new \Exception("Too much input data!");
+ }
+ $count_params = substr_count($post_data, "&");
+ if ($max_params > 0 && $count_params > $max_params) {
+ throw new \Exception("Too many input parameters!");
+ }
+ self::$string_of_POST_data = $post_data;
+ }
+ }
+
+ public static function clear_post_data() {
+ self::$string_of_POST_data = "";
+ }
+
+ public static function convert_to_utf8(string $in_str): string {
+ if (! extension_loaded('mbstring')) {
+ return $in_str;
+ }
+ $cur_encoding = mb_detect_encoding($in_str);
+ if($cur_encoding == "UTF-8" && mb_check_encoding($in_str,"UTF-8")) {
+ return $in_str;
+ } else {
+ return mb_convert_encoding($in_str, 'UTF-8', $cur_encoding);
+ }
+ }
+
+ // Escape HTML output
+ public static function h(string $string): string {
+ $utf8 = self::convert_to_utf8($string);
+ return htmlspecialchars($utf8, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
+ }
+
+ // Reverse encode of HTML
+ public static function html_decode(string $string): string {
+ return htmlspecialchars_decode($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
+ }
+
+ // HTML Purify library
+ public static function p(string $string): string {
+ $purifer = registry::get('di')->get_service('html_filter');
+ if (!$purifer->has_loaded()) {
+ $purifer->set_defaults();
+ }
+ return $purifer->purify($string);
+ }
+
+ // Escape JavaScript output
+ public static function j($input, int $levels_deep = 512): mixed {
+ try {
+ return json_encode($input, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, $levels_deep);
+ } catch (\JsonException $ex) {
+ return $ex;
+ }
+ }
+
+ public static function json_decode(string $string, bool $return_as_an_array = true, int $levels_deep = 512): mixed {
+ try {
+ return json_decode($string, $return_as_an_array, $levels_deep, JSON_THROW_ON_ERROR);
+ } catch (\JsonException $ex) {
+ return $ex;
+ }
+ }
+
+ public static function has_json_error($object): bool {
+ return ($object instanceof \JsonException);
+ }
+
+ // Escape URL output
+ public static function u(string $string): string {
+ return urlencode($string);
+ }
+
+ /*
+ * Encode HTML kindof... The problem with htmlentities() is that it is not
+ * very powerful, in fact, it does not escape single quotes, cannot detect
+ * the character set and does not validate HTML as well.
+ */
+ public static function e(string $string): string {
+ $utf8 = self::convert_to_utf8($string);
+ return htmlentities($utf8, ENT_QUOTES, 'UTF-8');
+ }
+
+ public static function de(string $data): string {
+ return html_entity_decode($data);
+ }
+
+ /*
+ * Note: Generally, "strip_tags" is just the wrong function.
+ * Never use it. And if you do, absolutely never use the second parameter,
+ * because sooner or later someone will abuse it.
+ */
+
+ public static function get_clean_server_var(string $var): mixed {
+ return filter_input(INPUT_SERVER, $var, FILTER_UNSAFE_RAW);
+ }
+
+ public static function get_bool($in): bool {
+ return (filter_var($in, FILTER_VALIDATE_BOOLEAN));
+ }
+
+ /**
+ * Purpose: To decode JQuery encoded objects, arrays, strings, int, bool types.
+ * The content must be of application/json.
+ * Note: It will return null if not valid json. false is not application/json
+ */
+ private static function get_json_post_data(
+ string $input_field_name,
+ bool $return_as_array = true,
+ int $levels_deep = 512
+ ) {
+ $ret_json = self::json_decode(
+ self::$string_of_POST_data,
+ $return_as_array,
+ $levels_deep
+ );
+ if (self::has_json_error($ret_json)) {
+ return false;
+ }
+ if (isset($ret_json[$input_field_name])) {
+ return $ret_json[$input_field_name];
+ }
+ return false;
+ }
+
+ private static function get_post_data(): \Generator {
+ $pairs = explode("&", self::$string_of_POST_data);
+ while(true) {
+ $pair = array_pop($pairs);
+ if ($pair === null) {
+ break;
+ }
+ $nv = explode("=", $pair);
+ $n = $nv[0] ?? false;
+ $v = $nv[1] ?? "";
+ unset($nv);
+ if ($n === false || empty($n)) {
+ continue;
+ }
+ $cmd = (yield urldecode($n) => urldecode($v));
+ if ($cmd == "stop") {
+ break;
+ }
+ }
+ unset($n);
+ unset($v);
+ unset($pairs);
+ }
+
+ private static function safer_html(string $input, HTML_FLAG $safety_level = HTML_FLAG::escape): string {
+ switch ($safety_level) {
+ case HTML_FLAG::raw :
+ throw new \Exception('Raw HTML not supported!');
+ case HTML_FLAG::strip :
+ return strip_tags($input);
+ case HTML_FLAG::encode :
+ return self::e($input);
+ case HTML_FLAG::purify :
+ return self::p($input);
+ case HTML_FLAG::escape :
+ default:
+ return self::h($input);
+ }
+ }
+
+ private static function t($item, bool $do_trim = true) {
+ if ($do_trim) {
+ if (is_string($item)) {
+ return trim($item);
+ }
+ if (\bs_tts\common::get_count($data)) {
+ $ret = [];
+ foreach($data as $text) {
+ if (is_bool($text) || is_int($text)) {
+ $ret[] = $text;
+ continue;
+ }
+ if (! is_string($text)) {
+ continue; // Deny Arrays and Objects here!
+ }
+ $ret[] = trim($text);
+ }
+ return $ret;
+ }
+ }
+ return $item;
+ }
+
+ private static function find_post_field(string $input_field_name): mixed {
+ $content_type = self::get_clean_server_var('CONTENT_TYPE');
+ if ($content_type === null) {
+ return false;
+ }
+ if (str_contains($content_type, "application/json")) {
+ return self::get_json_post_data($input_field_name);
+ }
+ if (str_contains($content_type, "application/x-www-form-urlencoded")) {
+ $post = self::get_post_data();
+ foreach($post as $key => $data) {
+ if ($key === $input_field_name) {
+ $post->send("stop"); // Break loop in Generator
+ return $data;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static function find_get_field(string $input_field_name): mixed {
+ if (isset($_SERVER['QUERY_STRING'])) {
+ $query = self::get_clean_server_var('QUERY_STRING');
+ $get = [];
+ parse_str($query, $get);
+ if (isset($get[$input_field_name])) {
+ return $get[$input_field_name];
+ }
+ }
+ return false;
+ }
+
+ private static function get_input_by_type(
+ string $input_field_name,
+ INPUTS $input_type,
+ ): mixed {
+ /* Must return here to avoid Failing Resolve later on in this FN
+ * as input types variable, debugging, and json will not Resolve!
+ */
+ if ($input_type == INPUTS::variable) {
+ return self::get_data_input($input_field_name);
+ }
+ if ($input_type == INPUTS::debugging) {
+ $rd = self::find_post_field($input_field_name);
+ if ($rd !== false) {
+ return $rd;
+ }
+
+ $is_set = filter_has_var(INPUT_POST, $input_field_name);
+ if ($is_set) {
+ return filter_input(INPUT_POST, $input_field_name);
+ }
+ if (!self::find_get_field("debugging")) {
+ return null;
+ }
+ $get_var = self::find_get_field($input_field_name);
+ if ($get_var !== false) {
+ return $get_var;
+ }
+ $is_get_set = filter_has_var(INPUT_GET, $input_field_name);
+ if ($is_get_set) {
+ return filter_input(INPUT_GET, $input_field_name);
+ }
+ return null;
+ }
+ if ($input_type === INPUTS::json) {
+ $rd = self::find_post_field($input_field_name);
+ if ($rd !== false) {
+ return $rd;
+ }
+ return null;
+ }
+ if ($input_type === INPUTS::get) {
+ $get_var = self::find_get_field($input_field_name);
+ if ($get_var !== false) {
+ return $get_var;
+ }
+ }
+ $resolve_input = $input_type->resolve();
+ $is_set = filter_has_var($resolve_input, $input_field_name);
+ if ($is_set) {
+ return filter_input($resolve_input, $input_field_name);
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param string $data
+ * @param array $a['html'] of type HTML_FLAG
+ * @return string|bool
+ */
+ private static function get_safer_string(string $data, use_io $a): string | bool {
+ if (isset($a->escape_html) && $a->escape_html instanceof \UnitEnum) {
+ return self::safer_html($data, $a->escape_html);
+ }
+ return self::safer_html($data);
+ }
+
+ private static function get_safer_html($data, use_io $a) {
+ if (is_string($data)) {
+ return self::get_safer_string($data, $a);
+ } else if (common::get_count($data)) {
+ $ret = [];
+ foreach($data as $text) {
+ if (is_bool($text) || is_int($text)) {
+ $ret[] = $text;
+ continue;
+ }
+ if (! is_string($text)) {
+ continue; // Deny Arrays and Objects here!
+ }
+ $ret[] = self::get_safer_string($text, $a);
+ }
+ return $ret;
+ }
+ return $data;
+ }
+
+ public static function required_fields_were_NOT_all_submitted(array $data): bool {
+ $field = $data['name'] ?? false;
+ $empty = $data['meta'][$field]['empty'] ?? true;
+ $required = $data['meta'][$field]['validation_rules_set'] ?? false;
+ return ($empty && $required);
+ }
+
+ private static function sanitize_helper(
+ string $from,
+ string $input_field_name,
+ use_io $a,
+ FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
+ bool $trim = true,
+ ) : array {
+
+ $meta = [];
+ $meta['missing'] = [];
+ $safer_data = "";
+ $rules = [];
+ $messages = [];
+
+ if (isset($a->field_filter) && $a->field_filter instanceof \UnitEnum) {
+ $field_type = $a->field_filter;
+ } else {
+ $field_type = $default_filter;
+ }
+
+ if (isset($a->input_var)) {
+ $user_text = $a->input_var;
+ } elseif (isset($a->input_type) && $a->input_type instanceof \UnitEnum) {
+ $user_text = self::get_input_by_type($input_field_name, $a->input_type);
+ } else {
+ $ret['name'] = $input_field_name;
+ $ret['meta']['missing'][] = $input_field_name;
+ $ret['errors'][$input_field_name] = "Missing Field $input_field_name";
+ $ret['html'] = null;
+ $ret['db'] = false;
+ $ret['logic'] = false;
+ return $ret;
+ }
+
+ $safer_data = false; // needs to be false to fail the validator
+ $safer_html_data = null; // should be null for ?? operator to work with it....
+
+ if (isset($a->validation_rule)) {
+ $rules[$input_field_name] = $a->validation_rule;
+ }
+
+ if (isset($a->validation_message) && isset($a->validation_rule)) {
+ $messages[$input_field_name] = $a->validation_message;
+ }
+
+ $meta[$input_field_name]['validation_rules_set'] = (count($rules)) ? true : false;
+
+ $db = (isset($a->skip_the_db)) ? $a->skip_the_db : false;
+ $meta[$input_field_name]['type'] = $field_type->name;
+ $meta[$input_field_name]['skip_db'] = $db;
+
+ if ($user_text === null) {
+ $safer_data = null;
+ $safer_db_data = null;
+ $safer_html_data = null;
+ $meta[$input_field_name]['empty'] = true;
+ } else {
+ $field_filter_resolved = $field_type->resolve();
+
+ $meta[$input_field_name]['empty'] = false;
+
+ $safer_data = $user_text;
+ if ($field_type == FIELD_FILTER::email) {
+ $safer_data = substr($safer_data, 0, 254);
+ }
+
+ $safer_data = filter_var($safer_data, FILTER_DEFAULT, $field_filter_resolved);
+
+ // FallBack: These field types should never allow arrays anyways
+ if ($field_type == FIELD_FILTER::raw_string ||
+ $field_type == FIELD_FILTER::raw
+ ) {
+ if (common::get_count($safer_data)) {
+ $safer_data = $safer_data[0];
+ }
+ }
+
+ if ($from === "html") {
+ $safer_html = self::get_safer_html($safer_data, $a);
+ if ($safer_html !== false) {
+ $safer_html_data = $safer_html;
+ }
+
+ if (isset($safer_html_data)) {
+ $safer_html_data = self::t($safer_html_data, $trim);
+ }
+ } else {
+ $safer_data = self::t($safer_data, $trim);
+ }
+
+ if ($field_type == FIELD_FILTER::integer_number) {
+ $safer_data = intval($safer_data);
+ }
+ if ($field_type == FIELD_FILTER::floating_point) {
+ $safer_data = floatval($safer_data);
+ }
+ if ($from === "db") {
+ if ($field_type == FIELD_FILTER::integer_number || $field_type == FIELD_FILTER::floating_point) {
+ $safer_db_data = $safer_data;
+ } else {
+ if (isset($a->use_db_filter) && $a->use_db_filter == DB_FILTER::ON) {
+ $safe_for_db = \tts\extras\safer_sql::get_safer_sql_text($safer_data);
+ $text = $safe_for_db["text"];
+ $meta[$input_field_name]['db_filter_status'] = $safe_for_db["status"] ?? \tts\SQL_SAFETY_FLAG::filtered;
+ } else {
+ $text = $safer_data;
+ }
+ $safer_db_data = $text;
+ }
+ }
+ }
+ $ret['name'] = $input_field_name;
+ $ret['meta'] = $meta;
+ if ($from === "db") {
+ $ret['db'] = $safer_db_data;
+ $data[$input_field_name] = $safer_db_data;
+ } elseif ($from === "logic") {
+ $ret['logic'] = $safer_data;
+ $data[$input_field_name] = $safer_data;
+ } elseif ($from === "html") {
+ $ret['html'] = $safer_html_data;
+ $data[$input_field_name] = $safer_html_data;
+ }
+ $ret['errors'] = (count($rules)) ? \CodeHydrater\validator::validate($data, $rules, $messages) : [];
+ return $ret;
+ }
+
+ /**
+ * As PHP uses the underlying C functions for filesystem related operations,
+ * it may handle null bytes in a quite unexpected way.
+ * As null bytes denote the end of a string in C, strings
+ * containing them won't be considered entirely but rather
+ * only until a null byte occurs. So, clean it out to
+ * avoid vulnerable code.
+ */
+ public static function remove_null_byte(string $input): string {
+ return str_replace(chr(0), '', $input);
+ }
+
+ public static function db_sanitize(
+ array $inputs,
+ FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
+ bool $trim = true,
+ ) : \Generator {
+ foreach ($inputs as $input_field_name => $a) {
+ if (! $a instanceof use_io) {
+ continue;
+ }
+ $yield = static::sanitize_helper(
+ "db",
+ $input_field_name,
+ $a,
+ $default_filter,
+ $trim
+ );
+ yield $yield;
+ }
+ }
+
+ public static function logic_sanitize(
+ array $inputs,
+ FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
+ bool $trim = true,
+ ) : \Generator {
+ foreach ($inputs as $input_field_name => $a) {
+ if (! $a instanceof use_io) {
+ continue;
+ }
+ $yield = static::sanitize_helper(
+ "logic",
+ $input_field_name,
+ $a,
+ $default_filter,
+ $trim
+ );
+ yield $yield;
+ }
+ }
+
+ /**
+ * Sanitize the inputs based on the rules an optionally trim the string
+ * @param FIELD_FILTER $default_filter FILTER_SANITIZE_STRING
+ * @param bool $trim
+ * @return Generator
+ */
+ public static function html_escape_and_sanitize(
+ array $inputs,
+ FIELD_FILTER $default_filter = FIELD_FILTER::raw_string,
+ bool $trim = true,
+ ) : \Generator {
+ foreach ($inputs as $input_field_name => $a) {
+ if (! $a instanceof use_io) {
+ continue;
+ }
+ $yield = static::sanitize_helper(
+ "html",
+ $input_field_name,
+ $a,
+ $default_filter,
+ $trim
+ );
+ yield $yield;
+ }
+ }
+
+}
diff --git a/src/bootstrap/siteHelper.php b/src/bootstrap/siteHelper.php
new file mode 100644
index 0000000..fc00b2b
--- /dev/null
+++ b/src/bootstrap/siteHelper.php
@@ -0,0 +1,222 @@
+
+ * @copyright (c) 2025, Robert Strutts
+ * @license MIT
+ */
+
+namespace CodeHydrater\bootstrap;
+
+final class siteHelper {
+
+ private static $ROOT;
+ private static $ROUTE;
+ private static $PRJ;
+ private static $FW_DIST;
+ private static $REQUEST_URI;
+ private static $REQUEST_METHOD;
+ private static $USE_SECURE = true;
+ private static $TESTING;
+ private static $queryParams;
+ private static $DEFAULT_PROJECT;
+ private static $all_projects = [];
+ private static $local_site_domains = ['localhost'];
+ private static $Private_IPs_allowed = ['127.0.0.1', '::1'];
+ private static $Public_IPs_allowed = [];
+
+ public static function set_local_site_domains(string|array $domain_name): void {
+ if (is_array($domain_name)) {
+ foreach($domain_name as $domain) {
+ self::$local_site_domains[] = $domain;
+ }
+ } elseif (is_string($domain_name)) {
+ self::$local_site_domains[] = $domain_name;
+ }
+ }
+
+ public static function set_allowed_Private_IPs(string|array $IP_addresses): void {
+ if (is_array($IP_addresses)) {
+ foreach($IP_addresses as $IP) {
+ $s_ip = \tts\security::get_valid_ip($IP);
+ if ($s_ip === false) {
+ continue;
+ }
+ self::$Private_IPs_allowed[] = $IP;
+ }
+ } elseif (is_string($IP_addresses)) {
+ $s_ip = \tts\security::get_valid_ip($IP);
+ if ($s_ip === false) {
+ return;
+ }
+ self::$Private_IPs_allowed[] = $IP_addresses;
+ }
+ }
+
+ public static function set_allowed_Public_IPs(string|array $IP_addresses): void {
+ if (is_array($IP_addresses)) {
+ foreach($IP_addresses as $IP) {
+ $s_ip = \tts\security::get_valid_public_ip($IP);
+ if ($s_ip === false) {
+ continue;
+ }
+ self::$Public_IPs_allowed[] = $s_ip;
+ }
+ } elseif (is_string($IP_addresses)) {
+ $s_ip = \tts\security::get_valid_public_ip($IP);
+ if ($s_ip === false) {
+ return;
+ }
+ self::$Public_IPs_allowed[] = $IP_addresses;
+ }
+ }
+
+ public static function get_route(): string {
+ return self::$ROUTE;
+ }
+
+ public static function get_root(): ?string {
+ return self::$ROOT;
+ }
+
+ public static function get_testing() {
+ return self::$TESTING;
+ }
+
+ public static function get_uri(): string {
+ return self::$REQUEST_URI;
+ }
+
+ public static function get_method(): string {
+ return strtoupper(self::$REQUEST_METHOD);
+ }
+
+ public static function get_params() {
+ return self::$queryParams;
+ }
+
+ public static function get_use_secure(): bool {
+ return self::$USE_SECURE;
+ }
+
+ /**
+ * Because $_SERVER['REQUEST_URI'] May only available on Apache,
+ * we generate an equivalent using other environment variables.
+ * @return string
+ */
+ public static function request_uri() {
+ if (self::$REQUEST_URI !== null && !empty(self::$REQUEST_URI)) {
+ $uri = self::$REQUEST_URI;
+ } else if (isset($_SERVER['REQUEST_URI'])) {
+ $uri = saferIO::get_clean_server_var('REQUEST_URI');
+ } else {
+ if (isset($_SERVER['argv'])) {
+ $uri = saferIO::get_clean_server_var('SCRIPT_NAME') . '?' . $_SERVER['argv'][0];
+ } elseif (isset($_SERVER['QUERY_STRING'])) {
+ $uri = saferIO::get_clean_server_var('SCRIPT_NAME') . '?' . \bs_tts\safer_io::get_clean_server_var('QUERY_STRING');
+ } else {
+ $uri = saferIO::get_clean_server_var('SCRIPT_NAME');
+ }
+ }
+ // Prevent multiple slashes to avoid cross site requests via the Form API.
+ $uri = '/' . ltrim($uri, '/');
+
+ return $uri;
+ }
+
+ public static function get_clean_server_var(string $var): mixed {
+ return filter_input(INPUT_SERVER, $var, FILTER_UNSAFE_RAW);
+ }
+
+ public static function site_url(): string {
+ $server_port = self::get_clean_server_var('SERVER_PORT');
+ $secure_port_on = self::get_clean_server_var('HTTPS');
+ $use_secure = ($server_port == '443' || $secure_port_on == 'on');
+ self::$USE_SECURE = $use_secure;
+ $protocol = ($use_secure) ? 'https://' : 'http://';
+ define('TTS_PROTOCOL', $protocol);
+ $domainName = self::get_clean_server_var('HTTP_HOST');
+
+ return $protocol . $domainName . "/";
+ }
+
+ public static function resolve($action, ...$params) {
+ if (is_callable($action)) {
+ return call_user_func($action, $params);
+ }
+
+ if (!is_array($action)) {
+ return false;
+ }
+
+ [$class, $method] = $action;
+ $call_class = "\\" . $class;
+
+ if (class_exists($call_class)) {
+ $auto_class = registry::get('di')->get_auto($call_class);
+ if (method_exists($call_class, $method)) {
+ return call_user_func_array([$auto_class, $method], $params);
+ }
+ }
+ return false;
+ }
+
+ private static function set_route(): void {
+ // Get just route
+ $pos = strpos(self::$REQUEST_URI, "?");
+ $uri = ($pos !== false) ? substr(self::$REQUEST_URI, 0, $pos) : self::$REQUEST_URI;
+ $root = str_replace(self::$ROOT, "", $uri);
+ $routes = explode('/', trim($root, '/'));
+ self::$ROUTE = implode('/', $routes);
+ }
+
+ private static function set_params(): void {
+ // Get just query string
+ $pos = strpos(self::$REQUEST_URI, "?");
+ $uri = ($pos !== false) ? substr(self::$REQUEST_URI, $pos + 1) : "";
+ if (empty($uri)) {
+ return;
+ }
+ $queryParams = [];
+ parse_str($uri, $queryParams);
+ self::$queryParams = $queryParams;
+ }
+
+ public static function restrictSite(): void {
+ if ($_SERVER['HTTP_REFERER'] != $_SERVER['HTTP_HOST']) {
+ die("Form may not be used outside of parent site!");
+ }
+ }
+
+ public static function get_cli_args(): string {
+ $argv = (isset($GLOBALS['argv'])) ? $GLOBALS['argv'] : [];
+ $args = array_shift($argv); // POP out the SCRIPT_NAME!!
+ if ($args === null) {
+ return ""; // NO Args
+ }
+ $route = $argv[0] ?? ""; // Keep the Route
+ $args = array_shift($argv); // POP out the ROUTE!!
+ if ($args === null) {
+ return $route;
+ }
+ return $route . "?" . ltrim(implode('&', $argv), "&");
+ }
+
+ public static function init(string $ROOT, string $REQUEST_URI, string $REQUEST_METHOD, bool $testing = false) {
+ self::$ROOT = $ROOT;
+ self::$REQUEST_URI = $REQUEST_URI;
+ self::$REQUEST_METHOD = $REQUEST_METHOD;
+ self::$TESTING = $testing;
+ self::set_route();
+ self::set_params();
+
+ if (! defined("ASSETS_BASE_REF")) {
+ define('ASSETS_BASE_REF', "/assets/");
+ }
+ define('SITE_URL', self::site_url());
+ define("BROWSER", self::get_clean_server_var('HTTP_USER_AGENT'));
+ define("ASSETS_DIR", "/public/assets/");
+ }
+}
\ No newline at end of file
diff --git a/src/classes/api.php b/src/classes/api.php
new file mode 100644
index 0000000..574e577
--- /dev/null
+++ b/src/classes/api.php
@@ -0,0 +1,354 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+namespace CodeHydrater;
+
+class api {
+ const CONTINUE_STATUS = "Continue"; // 100
+ const SWITCHING_PROTOCOLS = "Switching Protocols"; // 101
+ const OK = "OK"; // 200
+ const CREATED = "Created"; // 201
+ const ACCEPTED = "Accepted"; // 202
+ const NON_AUTHORITATIVE = "Non-Authoritative Information"; // 203
+ const NO_CONTENT = "No Content"; // 204
+ const RESET_CONTENT = "Reset Content"; // 205
+ const PARTIAL_CONTENT = "Partial Content"; // 206
+ const ALREADY_REPORTED = "Already Reported"; // 208
+ const MULTI_STATUS = "Multiple Choices"; // 300
+ const MOVED_PERMANENTLY = "Moved Permanently"; // 301
+ const MOVED_TEMPORARILY = "Moved Temporarily"; // 302
+ const SEE_OTHER = "See Other"; // 303
+ const NOT_MODIFIED = "Not Modified"; // 304
+ const USE_PROXY = "Use Proxy"; // 305
+ const TEMP_REDIRECT = "Temporary Redirect"; // 307
+ const BAD_REQUEST = "The request cannot be fulfilled due to bad syntax."; // 400
+ const UNAUTHORIZED = "The authorization details given appear to be invalid."; // 401
+ const PAYMENT_REQUIRED = "Payment Required"; // 402
+ const FORBIDDEN = "The requested resource is not accessible."; // 403
+ const NOT_FOUND = "The requested resource does not exist."; // 404
+ const METHOD_NOT_ALLOWED = "Method Not Allowed"; // 405
+ const NOT_ACCEPTABLE = "Not Acceptable"; // 406
+ const PROXY_AUTH_REQUIRED = "Proxy Authentication Required"; // 407
+ const REQUEST_TIME_OUT = "Request Time-out"; // 408
+ const CONFLICT = "Conflict"; // 409
+ const GONE = "Gone"; // 410
+ const LENGTH_REQUIRED = "Length Required"; // 411
+ const PRECONDITION_FAILED = "Precondition Failed"; // 412
+ const REQUEST_ENTITY_TOO_LARGE = "Request Entity Too Large"; // 413
+ const REQUEST_URI_TOO_LARGE = "Request-URI Too Large"; // 414
+ const UNSUPPORTED_FORMAT = "The format requested is not supported by the server."; // 415
+ const EXPECTATION_FAILED = "Expectation Failed"; // 417
+ const INTERNAL_ERROR = "An unexpected error occured."; // 500
+ const NOT_IMPLEMENTED = "Not Implemented"; // 501
+ const BAD_GATEWAY = "Bad Gateway"; // 502
+ const MAINTENANCE_MODE = "The requested resource is currently unavailable due to maintenance."; // 503
+ const GATEWAY_TIME_OUT = "Gateway Time-out"; // 504
+ const HTTP_VERSION_NOT_SUPPORTED = "HTTP Version not supported"; // 505
+
+ /**
+ * Use Encoder, default JSON
+ * @param type $data
+ * @param type $status_code
+ */
+ public static function encode($data, $status_code): void {
+ $response_type = misc::request_var('return');
+ switch ($response_type) {
+ case 'xml':
+ self::xml_encode($data, $status_code, null);
+ break;
+ case 'php':
+ self::php_encode($data, $status_code);
+ break;
+ case 'json':
+ default:
+ self::json_encode($data, $status_code);
+ break;
+ }
+ }
+
+ /**
+ * See: HTTP_status_codes.txt
+ * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ * HTTP $code to try, use $default if not valid
+ */
+ public static function get_code_number(int $code, int $default): int {
+ return ($code > 99 && $code < 600) ? $code : $default;
+ }
+
+ /**
+ * Encode Error
+ * @param array $data
+ */
+ public static function error(array $data): void {
+ $error_code = (isset($data['code'])) ? $data['code'] : 400;
+
+ $code = self::get_code_number($error_code, 400);
+ $data['result'] = false;
+
+ if (isset($data['response'])) {
+ switch ($data['response']) {
+ case self::CONTINUE_STATUS:
+ $long_code = "100 Continue";
+ $data['message'] = self::CONTINUE_STATUS;
+ break;
+ case self::SWITCHING_PROTOCOLS:
+ $long_code = "101 Switching Protocols";
+ $data['message'] = self::SWITCHING_PROTOCOLS;
+ break;
+ case self::MULTI_STATUS:
+ $long_code = "300 Multiple Choices";
+ $data['message'] = self::MULTI_STATUS;
+ break;
+ case self::MOVED_PERMANENTLY:
+ $long_code = "301 Moved Permanently";
+ $data['message'] = self::MOVED_PERMANENTLY;
+ break;
+ case self::MOVED_TEMPORARILY:
+ $long_code = "302 Moved Temporarily";
+ $data['message'] = self::MOVED_TEMPORARILY;
+ break;
+ case self::SEE_OTHER:
+ $long_code = "303 See Other";
+ $data['message'] = self::SEE_OTHER;
+ break;
+ case self::NOT_MODIFIED:
+ $long_code = "304 Not Modified";
+ $data['message'] = self::NOT_MODIFIED;
+ break;
+ case self::USE_PROXY:
+ $long_code = "305 Use Proxy";
+ $data['message'] = self::USE_PROXY;
+ break;
+ case self::TEMP_REDIRECT:
+ $long_code = "307 Temporary Redirect";
+ $data['message'] = self::TEMP_REDIRECT;
+ case self::BAD_REQUEST:
+ $long_code = "400 Bad Request";
+ $data['message'] = self::BAD_REQUEST;
+ break;
+ case self::UNAUTHORIZED:
+ $long_code = "401 Unauthorized";
+ $data['message'] = self::UNAUTHORIZED;
+ break;
+ case self::PAYMENT_REQUIRED:
+ $long_code = "402 Payment Required";
+ $data['message'] = self::PAYMENT_REQUIRED;
+ break;
+ case self::FORBIDDEN:
+ $long_code = "403 Forbidden";
+ $data['message'] = self::FORBIDDEN;
+ break;
+ case self::NOT_FOUND:
+ $long_code = "404 Not Found";
+ $data['message'] = self::NOT_FOUND;
+ break;
+ case self::METHOD_NOT_ALLOWED:
+ $long_code = "405 Method Not Allowed";
+ $data['message'] = self::METHOD_NOT_ALLOWED;
+ break;
+ case self::NOT_ACCEPTABLE:
+ $long_code = "406 Bad Request";
+ $data['message'] = self::NOT_ACCEPTABLE;
+ break;
+ case self::PROXY_AUTH_REQUIRED:
+ $long_code = "407 Proxy Authentication Required";
+ $data['message'] = self::PROXY_AUTH_REQUIRED;
+ break;
+ case self::REQUEST_TIME_OUT:
+ $long_code = "408 Request Time-out";
+ $data['message'] = self::REQUEST_TIME_OUT;
+ break;
+ case self::CONFLICT:
+ $long_code = "409 Bad Request";
+ $data['message'] = self::CONFLICT;
+ break;
+ case self::GONE:
+ $long_code = "410 Gone";
+ $data['message'] = self::GONE;
+ break;
+ case self::LENGTH_REQUIRED:
+ $long_code = "411 Length Required";
+ $data['message'] = self::LENGTH_REQUIRED;
+ break;
+ case self::PRECONDITION_FAILED:
+ $long_code = "412 Precondition Failed";
+ $data['message'] = self::PRECONDITION_FAILED;
+ break;
+ case self::REQUEST_ENTITY_TOO_LARGE:
+ $long_code = "413 Request Entity Too Large";
+ $data['message'] = self::REQUEST_ENTITY_TOO_LARGE;
+ break;
+ case self::REQUEST_URI_TOO_LARGE:
+ $long_code = "414 Request-URI Too Large";
+ $data['message'] = self::REQUEST_URI_TOO_LARGE;
+ break;
+ case self::UNSUPPORTED_FORMAT:
+ $long_code = "415 Unsupported Media Type";
+ $data['message'] = self::UNSUPPORTED_FORMAT;
+ break;
+ case self::EXPECTATION_FAILED:
+ $long_code = "417 Expectation Failed";
+ $data['message'] = self::EXPECTATION_FAILED;
+ break;
+ case self::INTERNAL_ERROR:
+ $long_code = "500 Internal Server Error";
+ $data['message'] = self::INTERNAL_ERROR;
+ break;
+ case self::NOT_IMPLEMENTED:
+ $long_code = "501 Not Implemented";
+ $data['message'] = self::NOT_IMPLEMENTED;
+ break;
+ case self::BAD_GATEWAY:
+ $long_code = "502 Bad Gateway";
+ $data['message'] = self::BAD_GATEWAY;
+ break;
+ case self::MAINTENANCE_MODE:
+ $long_code = "503 Service Unavailable";
+ $data['message'] = self::MAINTENANCE_MODE;
+ break;
+ case self::GATEWAY_TIME_OUT:
+ $long_code = "504 Gateway Time-out";
+ $data['message'] = self::GATEWAY_TIME_OUT;
+ break;
+ case self::HTTP_VERSION_NOT_SUPPORTED:
+ $long_code = "505 HTTP Version not supported";
+ $data['message'] = self::HTTP_VERSION_NOT_SUPPORTED;
+ break;
+ default:
+ $long_code = $code;
+ break;
+ }
+ } else {
+ $long_code = $code;
+ }
+
+ $data['code'] = $long_code;
+
+ $memory_check = bootstrap\common::get_bool(\tts\misc::request_var('debug'));
+ if ($memory_check) {
+ $echo = false;
+ $data['memory_used'] = memory_usage::get_memory_stats($echo);
+ }
+
+ self::encode($data, $long_code);
+ }
+
+ /**
+ * Encode ok
+ * @param array $data
+ */
+ public static function ok(array $data = array()): void {
+ $data['result'] = true;
+ $code = 200; // OK
+
+ $memory_check = bootstrap\common::get_bool(misc::request_var('debug'));
+ if ($memory_check) {
+ $echo = false;
+ $data['memory_used'] = memory_usage::get_memory_stats($echo);
+ }
+
+ if (isset($data['code'])) {
+ if ($data['code'] > 199 && $data['code'] < 209) {
+ $code = $data['code'];
+ }
+ unset($data['code']);
+ }
+
+ if (isset($data['response'])) {
+ switch ($data['response']) {
+ case self::CREATED: $long_code = "201 Created"; break;
+ case self::ACCEPTED: $long_code = "202 Accepted"; break;
+ case self::NON_AUTHORITATIVE: $long_code = "203 Non-Authoritative Information"; break;
+ case self::NO_CONTENT: $long_code = "204 No Content"; break;
+ case self::RESET_CONTENT: $long_code = "205 Reset Content"; break;
+ case self::PARTIAL_CONTENT: $long_code = "206 Partial Content"; break;
+ case self::ALREADY_REPORTED: $long_code = "208 Already Reported"; break;
+ case self::OK: $long_code = "200 OK"; break;
+ default: $long_code = $code; break;
+ }
+ } else {
+ $long_code = $code;
+ }
+
+ self::encode($data, $long_code);
+ }
+
+ public static function xml_encode($data, $status_code, $object = null, string $start_tag = 'root') {
+ if (is_null($object)) {
+ $xml = new \SimpleXMLElement('<'.$start_tag.'/>');
+ self::xml_encode($status_code, $xml, $data);
+
+ if (!headers_sent()) {
+ header($_SERVER['SERVER_PROTOCOL'] . " " . $status_code);
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header('Content-Type: text/xml; charset=utf-8', true, intval($status_code));
+ }
+
+ echo $xml->asXML();
+ exit;
+ } else {
+ foreach ($data as $key => $value) {
+ if (is_array($value)) {
+ $new_object = $object->addChild($key);
+ self::xml_encode($value, $status_code, $new_object);
+ } else {
+ $object->addChild($key, $value);
+ }
+ }
+ }
+ }
+
+ /*
+ * Purpose to decode XML into an array
+ */
+
+ public static function xml_decode(string $xmlstring) {
+ $xml = simplexml_load_string($xmlstring);
+ $json = json_encode($xml);
+ return json_decode($json, true);
+ }
+
+ public static function xml_parse(string $htmlStr): string {
+ $xmlStr = str_replace('<', '<', $htmlStr);
+ $xmlStr = str_replace('>', '>', $xmlStr);
+ $xmlStr = str_replace('"', '"', $xmlStr);
+ $xmlStr = str_replace("'", ''', $xmlStr);
+ $xmlStr = str_replace("&", '&', $xmlStr);
+ return $xmlStr;
+ }
+
+ public static function json_encode($data, $status_code): void {
+ if (!headers_sent()) {
+ header($_SERVER['SERVER_PROTOCOL'] . " " . $status_code);
+ /*
+ * Allow JavaScript from anywhere. CORS - Cross Origin Resource Sharing
+ * @link https://manning-content.s3.amazonaws.com/download/f/54fa960-332e-4a8c-8e7f-1eb213831e5a/CORS_ch01.pdf
+ */
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header('Content-Type: application/json; charset=utf-8', true, intval($status_code));
+ }
+ echo json_encode($data);
+ exit;
+ }
+
+ public static function php_encode($data, $status_code): void {
+ if (!headers_sent()) {
+ header($_SERVER['SERVER_PROTOCOL'] . " " . $status_code);
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header('Content-Type: text/php; charset=utf-8', true, intval($status_code));
+ }
+ echo serialize($data);
+ exit;
+ }
+
+}
diff --git a/src/classes/app.php b/src/classes/app.php
new file mode 100644
index 0000000..259a4b9
--- /dev/null
+++ b/src/classes/app.php
@@ -0,0 +1,205 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+namespace CodeHydrater;
+
+use Exception;
+
+/**
+ *
+ * @todo Ensure tts JS error reporting works
+ * @todo Finish Session MGT, Encrypted Sessions
+ * @todo Make Cached Sessions
+ * @todo Force Database methods to try Cache First and Save Cache Data
+ */
+
+class app {
+ private $file;
+ private $class;
+ private $method;
+ private $params;
+
+ private function get_first_chunks(string $input): string {
+ $parts = explode('/', $input);
+ $last = array_pop($parts);
+ $parts = array(implode('/', $parts), $last);
+ return $parts[0];
+ }
+
+ private function get_last_part(string $input): string {
+ $reversedParts = explode('/', strrev($input), 2);
+ return strrev($reversedParts[0]);
+ }
+
+ /**
+ * Do not declare a return type here, as it will Error out!!
+ */
+ public function __construct() {
+ $full_route = bootstrap\siteHelper::get_route();
+ $no_hmtl = str_replace(".html", "", $full_route);
+
+ // Find the Route
+ $route = $this->get_first_chunks($no_hmtl);
+
+ // Find the Method
+ $the_method = $this->get_last_part($no_hmtl);
+
+ $params = bootstrap\siteHelper::get_params();
+
+ // Now load Route
+ $is_from_the_controller = true; // TRUE for from Constructor...
+ $this->router($route, $the_method, $params, $is_from_the_controller);
+ }
+
+ private function get_ctrl_dir(): string {
+ $ctrl = (consoleApp::is_cli()) ? "cli_" : "";
+ return (bootstrap\siteHelper::get_testing()) ? "test_" : $ctrl;
+ }
+
+ /**
+ * Gets route and checks if valid
+ * @param string $route
+ * @param string $method
+ * @param bool $is_controller
+ * @retval action
+ */
+ public function router(?string $route, ?string $method, $params, bool $is_controller = false) {
+ $ROOT = bootstrap\siteHelper::get_root();
+ $file = "";
+ $class = "";
+
+ if (misc::is_empty($route)) {
+ $uri = '/app/' . bootstrap\configure::get('CodeHydrater', 'default_project');
+ } else {
+ $uri = $route;
+ }
+
+ try {
+ $filtered_uri = security::filter_uri($uri);
+ } catch (Exception $ex) {
+ $this->local404(); // Route Un-Safe URI to Local 404 Page
+ }
+
+ $safe_folders = bootstrap\requires::filter_dir_path(
+ $this->get_first_chunks($filtered_uri)
+ );
+ if (bootstrap\requires::is_dangerous($safe_folders)) {
+ $this->local404();
+ }
+
+ $safe_folders = rtrim($safe_folders, '/');
+ if (empty($ROOT)) {
+ $this->local404();
+ }
+
+ $safe_file = bootstrap\requires::filter_dir_path(
+ $this->get_last_part($filtered_uri)
+ );
+ if (bootstrap\requires::is_dangerous($safe_file)) {
+ $this->local404();
+ }
+
+ $test = $this->get_ctrl_dir();
+ $dir = bootstrap\requires::safer_dir_exists($ROOT . "{$test}controllers/" . $safe_folders);
+
+ //check for default site controller first
+ if ($dir === false) {
+ $this->local404();
+ } else {
+ $file = bootstrap\requires::safer_file_exists(basename($safe_file) . '_ctrl.php', $dir);
+ if ($file !== false) {
+ $class = security::filter_class($safe_folders) . "\\" . security::filter_class($safe_file . "_ctrl");
+ } else {
+ $this->local404();
+ }
+ }
+
+ if (misc::is_empty($method)) {
+ $method = ""; // Clear out null if exists
+ }
+
+ if (substr($method, 0, 2) == '__') {
+ $method = ""; // Stop any magical methods being called
+ }
+
+ if ($is_controller === true) {
+ $this->file = $file;
+ $this->class = $class;
+ $this->method = $method;
+ $this->params = $params;
+ } else {
+ return $this->action($file, $class, $method, $params);
+ }
+ }
+
+ private function local404() {
+ page_not_found::error404();
+ }
+
+ /**
+ * Do controller action
+ * @param string $file
+ * @param string $class
+ * @param string $method
+ * @retval type
+ */
+ private function action(string $file, string $class, string $method, $params) {
+ $safer_file = bootstrap\requires::safer_file_exists($file);
+ if (! $safer_file) {
+ $this->local404();
+ }
+ if (empty($class)) {
+ $this->local404();
+ }
+
+ $use_api = misc::is_api();
+ $test = $this->get_ctrl_dir();
+
+ $call_class = "\\Project\\" . $test . 'controllers\\' . $class;
+ $controller = new $call_class();
+
+ if ($method === "error" && str_contains($class, "app") &&
+ method_exists($controller, $method) === false
+ ) {
+ CodeHydrater_broken_error();
+ return false;
+ }
+
+ if ($use_api) {
+ if (empty($method)) {
+ $method = "index";
+ }
+ $method .= "_api";
+ if (method_exists($controller, $method)) {
+ return $controller->$method($params);
+ } else {
+ page_not_found::error404_cli();
+ }
+ } else {
+ if (!empty($method) && method_exists($controller, $method)) {
+ return $controller->$method($params);
+ } else {
+ if (empty($method) && method_exists($controller, 'index')) {
+ return $controller->index($params);
+ } else {
+ $this->local404();
+ }
+ }
+ }
+ }
+
+ /**
+ * Does load controller by calling action
+ */
+ public function load_controller() {
+ return $this->action($this->file, $this->class, $this->method, $this->params);
+ }
+
+} // end of app
diff --git a/src/classes/consoleApp.php b/src/classes/consoleApp.php
new file mode 100644
index 0000000..523a0ec
--- /dev/null
+++ b/src/classes/consoleApp.php
@@ -0,0 +1,47 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+namespace CodeHydrater;
+
+final class consoleApp {
+ public static $is_cli = false;
+
+ public static function is_cli() {
+ if (static::$is_cli) {
+ return true;
+ }
+
+ if (defined('STDIN')) {
+ return true;
+ }
+
+ if (php_sapi_name() === 'cli') {
+ return true;
+ }
+
+ if (array_key_exists('SHELL', $_ENV)) {
+ return true;
+ }
+
+// $argv = $_SERVER['argv'] ?? [];
+// && count($argv) > 0
+
+ if (!isset($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT'])) {
+ return true;
+ }
+
+ if (!array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ return true;
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/classes/enums/safer_io_enums.php b/src/classes/enums/safer_io_enums.php
new file mode 100644
index 0000000..a349c6c
--- /dev/null
+++ b/src/classes/enums/safer_io_enums.php
@@ -0,0 +1,88 @@
+ INPUT_POST,
+ self::get => INPUT_GET,
+ self::cookie => INPUT_COOKIE,
+ self::env => INPUT_ENV,
+ self::server => INPUT_SERVER,
+ };
+ }
+}
+
+enum DB_FILTER {
+ case ON; // Tries to Filter out SQL from User Input
+ case OFF; // Normal pass thourgh...
+}
+
+enum FIELD_FILTER: string {
+ case raw_string = "string";
+ case array_of_strings = "strings";
+ case email = "email-address";
+ case url = "site-url";
+ case raw = "unfiltered-non-sanitized";
+ case integer_number = "integer";
+ case array_of_ints = "integers";
+ case floating_point = "float";
+ case array_of_floats = "floats";
+
+ public function resolve() {
+ return match($this) {
+ self::raw_string => FILTER_UNSAFE_RAW,
+ self::array_of_strings => [
+ 'filter' => FILTER_UNSAFE_RAW,
+ 'flags' => FILTER_REQUIRE_ARRAY
+ ],
+ self::email => FILTER_SANITIZE_EMAIL,
+ self::url => FILTER_SANITIZE_URL,
+ self::raw => FILTER_DEFAULT, // Unfiltered, non-sanitized!!!
+ self::integer_number => [
+ 'filter' => FILTER_SANITIZE_NUMBER_INT,
+ 'flags' => FILTER_REQUIRE_SCALAR
+ ],
+ self::array_of_ints => [
+ 'filter' => FILTER_SANITIZE_NUMBER_INT,
+ 'flags' => FILTER_REQUIRE_ARRAY
+ ],
+ self::floating_point => [
+ 'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
+ 'flags' => FILTER_FLAG_ALLOW_FRACTION
+ ],
+ self::array_of_floats => [
+ 'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
+ 'flags' => FILTER_REQUIRE_ARRAY
+ ],
+ };
+ }
+}
diff --git a/src/classes/exceptions/Bool_Exception.php b/src/classes/exceptions/Bool_Exception.php
new file mode 100644
index 0000000..38ce56b
--- /dev/null
+++ b/src/classes/exceptions/Bool_Exception.php
@@ -0,0 +1,14 @@
+getMessage() . ' is not a valid bool type.';
+ return $errorMsg;
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/exceptions/DB_Exception.php b/src/classes/exceptions/DB_Exception.php
new file mode 100644
index 0000000..cb957d8
--- /dev/null
+++ b/src/classes/exceptions/DB_Exception.php
@@ -0,0 +1,103 @@
+getMessage() .PHP_EOL;
+ } else {
+ $msg = $message;
+ }
+
+ $ajax = ($output === EX_OUTPUT::ajax ||
+ (self::$ouput === EX_OUTPUT::static &&
+ self::$ouput === EX_OUTPUT::ajax)
+ ) ? true : false;
+
+ $status_code = 500;
+ if (!headers_sent() && $ajax) {
+ header($_SERVER['SERVER_PROTOCOL'] . " " . $status_code);
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header("Content-Type: application/json; charset=UTF-8", true, intval($status_code));
+ }
+
+ $json = [];
+ $json['had_error'] = '1';
+ $json['success'] = false;
+ $json['user_message'] = $msg;
+ if ($ajax) {
+ echo json_encode($json);
+ }
+
+ if ($handle == HANDLE_FAILURE::always_throw ||
+ (self::$handle === HANDLE_FAILURE::always_throw &&
+ $handle === HANDLE_FAILURE::static)
+ ) {
+ throw new \Exception($msg);
+ }
+ if ($handle === HANDLE_FAILURE::halt ||
+ (self::$handle === HANDLE_FAILURE::halt &&
+ $handle === HANDLE_FAILURE::static)) {
+ die();
+ }
+ return self::$msg;
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/html.php b/src/classes/html.php
new file mode 100644
index 0000000..d4e3244
--- /dev/null
+++ b/src/classes/html.php
@@ -0,0 +1,130 @@
+value
+ * @param string $default Item to select on.
+ * @param string $select_by ['value'] for most of your needs.
+ * @param array $a as optional options
+ * @param bool $escape XSS prevention...
+ * @retval string of option values...
+ */
+ public static function do_options(
+ array $options,
+ string $default = '',
+ string $select_by = 'text',
+ array $a = array(),
+ bool $escape = true
+ ): string {
+ $more = '';
+ if (count($a)) {
+ foreach ($a as $k => $v) {
+ if ($escape) {
+ $more .= " ". bootstrap\saferIO::h($k) . "=\"" . bootstrap\saferIO::h($v) . "\"";
+ } else {
+ $more .= " {$k}=\"{$v}\"";
+ }
+ }
+ }
+
+ $values = '';
+ foreach ($options as $value => $text) {
+ $compair_to = ($select_by == 'text') ? $text : $value;
+ $selected = (!empty($default) && $default == $compair_to) ? 'selected' : '';
+ if ($escape) {
+ $value = bootstrap\saferIO::h($value);
+ $text = bootstrap\saferIO::h($text);
+ }
+ $values .= "";
+ }
+ return $values;
+ }
+
+ /**
+ * Used by Memory_Usage script.
+ * Displays a table from input arrays -- fields
+ * @param array $header_fields - TH
+ * @param array $fields - TD
+ * @param bool $escape XSS prevention...
+ * @retval void
+ */
+ public static function show_table(
+ array $header_fields,
+ array $fields,
+ bool $escape = true
+ ): void {
+ $nl = PHP_EOL;
+ echo "{$nl}{$nl}";
+ echo "\t{$nl}";
+ foreach($header_fields as $header_field) {
+ $field = ($escape) ? \bs_tts\safer_io::h($header_field) : $header_field;
+ echo "\t\t| {$field} | {$nl}";
+ }
+ echo "\t
{$nl}";
+
+ foreach($fields as $field) {
+ echo "\t{$nl}";
+ foreach($field as $td) {
+ $cell = ($escape) ? \bs_tts\safer_io::h($td) : $td;
+ echo "\t\t| {$cell} | {$nl}";
+ }
+ echo "\t
{$nl}";
+ }
+ echo "
{$nl}";
+ }
+
+ /**
+ * Generators use Memory in an Effient way!!!! So use this.
+ * @param array $header_fields
+ * @param array $db_field_names
+ * @param \Iterator $records
+ * @param bool $escape
+ * @return void
+ */
+ public static function show_table_from_generator(
+ array $header_fields,
+ \Iterator $records,
+ bool $escape = true
+ ): void {
+ $nl = PHP_EOL;
+ echo "{$nl}{$nl}";
+ echo "\t{$nl}";
+ foreach($header_fields as $header_field) {
+ $field = ($escape) ? \bs_tts\safer_io::h($header_field) : $header_field;
+ echo "\t\t| {$field} | {$nl}";
+ }
+ echo "\t
{$nl}";
+
+ foreach($records as $record) {
+ echo "\t{$nl}";
+ foreach($record as $key => $value) {
+ if (! is_string($key)) {
+ continue; // Remove Duplicate records
+ }
+ $td = $value ?? "";
+ if (is_string($td)) {
+ $cell = ($escape) ? \bs_tts\safer_io::h($td) : $td;
+ } else {
+ $cell = (string) $td;
+ }
+ echo "\t\t| {$cell} | {$nl}";
+ }
+ echo "\t
{$nl}";
+ }
+ echo "
{$nl}";
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/makeLicenseFiles.php b/src/classes/makeLicenseFiles.php
new file mode 100644
index 0000000..0c71157
--- /dev/null
+++ b/src/classes/makeLicenseFiles.php
@@ -0,0 +1,31 @@
+
+ * @copyright (c) 2025, Robert Strutts
+ * @license MIT
+ */
+
+namespace CodeHydrater;
+
+use HydraterLicense\MakeLicense;
+
+const PrivatePEM = BaseDir."/keydata/private.pem";
+const PublicPEM = BaseDir."/keydata/public.pem";
+const AESKeysFile = BaseDir."/src/aeskeys.php";
+const LicenseFile = BaseDir."/keydata/license.json";
+
+if (! class_exists("HydraterLicense\MakeLicense")) {
+ die("Sorry, this extenstion is not availiable!");
+}
+
+$license_maker->generateLicense(
+ Array_For_Files,
+ ALLOWED_DOMAINS,
+ PrivatePEM,
+ PublicPEM,
+ AESKeysFile,
+ LicenseFile
+);
\ No newline at end of file
diff --git a/src/classes/memory_usage.php b/src/classes/memory_usage.php
new file mode 100644
index 0000000..4703d36
--- /dev/null
+++ b/src/classes/memory_usage.php
@@ -0,0 +1,71 @@
+Total Diff', "{$s_diff}");
+ $a_fields[] = array(' ', ' ');
+ $a_fields[] = array('PEAK', $s_peak);
+ $a_fields[] = array('Total Diff PEAK', "{$s_diff_peak}");
+ }
+
+ if ($echo === true) {
+ html::show_table($a_headers, $a_fields, false);
+ } else {
+ return $a_fields;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/misc.php b/src/classes/misc.php
new file mode 100644
index 0000000..44633a3
--- /dev/null
+++ b/src/classes/misc.php
@@ -0,0 +1,370 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+namespace CodeHydrater;
+
+final class misc {
+
+ public static function post_var(string $var, int $filter = FILTER_UNSAFE_RAW): mixed {
+ return filter_input(INPUT_POST, $var, $filter);
+ }
+
+ public static function get_var(string $var, int $filter = FILTER_UNSAFE_RAW): mixed {
+ return filter_input(INPUT_GET, $var, $filter);
+ }
+
+ public static function request_var(string $var, int $filter = FILTER_UNSAFE_RAW): mixed {
+ if (filter_has_var(INPUT_POST, $var)) {
+ return self::post_var($var, $filter);
+ }
+ if (filter_has_var(INPUT_GET, $var)) {
+ return self::get_var($var, $filter);
+ }
+ return "";
+ }
+
+ /**
+ * Please note that not all web servers support: HTTP_X_REQUESTED_WITH
+ * So, you need to code more checks!
+ * @retval boolean true if AJAX request by JQuery, etc...
+ */
+ public static function is_ajax(): bool {
+ $http_x_requested_with = bootstrap\saferIO::get_clean_server_var('HTTP_X_REQUESTED_WITH');
+ return (self::compair_it($http_x_requested_with, 'xmlhttprequest'));
+ }
+
+ /**
+ * Checks if two strings match, case-insensitive
+ * @param string $var
+ * @param string $var2
+ * @retval bool true match
+ */
+ public static function compair_it(?string $var, ?string $var2): bool {
+ if ($var === null || $var2 === null) {
+ return false;
+ }
+ return (bootstrap\common::string_to_lowercase(trim($var)) === bootstrap\common::string_to_lowercase(trim($var2)));
+ }
+
+ /**
+ * Multi-Bytes Safe Lowercase and Trim data
+ * @param string $var
+ * @retval string
+ */
+ public static function safe_lowercase_trim(string $var): string {
+ return (bootstrap\common::string_to_lowercase(trim($var)));
+ }
+
+ /**
+ * Is valid ID > zero
+ * @param int $var
+ * @retval bool is valid ID
+ */
+ public static function is_valid_id(int $var): bool {
+ return ($var > 0) ? true : false;
+ }
+
+ /**
+ * Is NOT a valid ID... (ID < 1)
+ * @param int $var
+ * @retval bool true if not valid ID
+ */
+ public static function is_not_valid_id(int $var): bool {
+ return ($var < 1) ? true : false;
+ }
+
+ /**
+ * Check if string is not empty
+ * @param string $var
+ * @retval bool
+ */
+ public static function is_not_empty(?string $var): bool {
+ return ($var !== null && !empty(trim($var)));
+ }
+
+ /**
+ * Check if string is empty
+ * @param string $var
+ * @retval bool
+ */
+ public static function is_empty(?string $var): bool {
+ return ($var === null || empty(trim($var)) );
+ }
+
+ /**
+ * RESTFUL method detection for API
+ */
+ public static function get_request_method(): array {
+ $method = filter_input(INPUT_SERVER, 'REQUEST_METHOD', FILTER_SANITIZE_ENCODED);
+ switch (strtoupper($method)) {
+ case 'GET': // only retrieve data
+ $crud = 'read';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'HEAD': // Has no response body
+ $crud = 'read';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'POST':
+ $crud = 'create';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'PUT':
+ $crud = 'replace';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'PATCH':
+ $crud = 'modify';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'DELETE':
+ $crud = 'delete';
+ return ['method' => $method, 'crud' => $crud, 'resetful' => true, 'valid' => true];
+ case 'CONNECT':
+ case 'COPY':
+ case 'OPTIONS':
+ case 'TRACE':
+ case 'LINK':
+ case 'UNLINK':
+ case 'PURGE':
+ case 'LOCK':
+ case 'UNLOCK':
+ case 'PROPFIND':
+ case 'VIEW':
+ return ['method' => $method, 'crud' => false, 'restful' => false, 'valid' => true];
+ default:
+ return ['method' => $method, 'crud' => false, 'resetful' => false, 'valid' => false];
+ }
+ }
+
+ /**
+ * Fetch File
+ * @param string $var
+ * @retval content or :null
+ */
+ public static function file_var(string $var) {
+ return (isset($_FILES[$var])) ? $_FILES[$var] : ':null';
+ }
+
+ /**
+ * How to prevent CRLF injection (Http response HEADER splitting) in php
+ * https://stackoverflow.com/questions/31318151/how-to-prevent-crlf-injection-http-response-splitting-in-php
+ * https://medium.com/@tomnomnom/crlf-injection-into-phps-curl-options-e2e0d7cfe545
+ */
+ public static function abort_on_crlf(string $data): string {
+// if (self::$_safe_replacement_for_crlf) {
+ $data = str_ireplace(["%0d", "%0a"], "", $data);
+// }
+
+ $cr = "/%0D|%0d/";
+ $lf = "/%0A|%0a/";
+
+ $cr_check = preg_match($cr, $data);
+ $lf_check = preg_match($lf, $data);
+
+ if ($cr_check > 0 || $lf_check > 0) {
+ throw new \Exception('Attempted: CRLF header hack detected. Aborting.');
+ }
+ return $data;
+ }
+
+ public static function filter_two_decimal_number($number): string {
+ return number_format(filter_var($number, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION), 2);
+ }
+
+ /**
+ * Filter/Sanitize Email data
+ * @param string $data
+ * @retval string of sanitized email data or :false
+ */
+ public static function clean_email(string $data): string {
+ $email = filter_var(trim($data), FILTER_SANITIZE_EMAIL);
+ return (filter_var($email, FILTER_VALIDATE_EMAIL)) ? $email : ':false';
+ }
+
+ /**
+ * Encode and Filter/Sanitize URL data
+ * @param string $data
+ * @retval string of sanitized url data
+ */
+ public static function encode_url_var(string $data): string {
+ return urlencode(filter_var(trim($data), FILTER_SANITIZE_URL));
+ }
+
+ public static function is_url(string $url): bool {
+ return filter_var($url, FILTER_VALIDATE_URL);
+ }
+
+ /**
+ * Decode URL data
+ * @param string $data
+ * @retval string of url data
+ */
+ public static function decode_url_var(string $data): string {
+ return trim(urldecode($data));
+ }
+
+ public static function is_regex($exp): bool {
+ return (filter_var($exp, FILTER_VALIDATE_REGEXP));
+ }
+
+ public static function email_hyper_link(string $address, string $subject, string $body): string {
+ $mail_to_uri = 'mailto:' . rawurlencode($address) . '?subject=' . rawurlencode($subject) . '&body=' . rawurlencode($body);
+ return htmlspecialchars($mail_to_uri);
+ }
+
+ // GET requests should not make changes
+ // Only POST requests should make changes
+
+ public static function request_is_get() {
+ return $_SERVER['REQUEST_METHOD'] === 'GET';
+ }
+
+ public static function request_is_post() {
+ return $_SERVER['REQUEST_METHOD'] === 'POST';
+ }
+
+
+ /**
+ * Helper function for safe_url -> does URL encode
+ * @param string $q
+ * @retval string
+ */
+ private static function _parm_encode(string $q): string {
+ if (substr_count($q, "=") > 0) {
+ $parms = explode("=", $q);
+ $parm = self::encode_url_var($parms[0]);
+ $value = self::encode_url_var($parms[1]);
+ return $parm . "=" . $value . "&";
+ } else {
+ return self::encode_url_var($q);
+ }
+ }
+
+ /**
+ * Encode and make safe URL
+ * @param string $vars
+ * @retval string the safe url
+ */
+ public static function safe_url(string $vars): string {
+ $new = '';
+
+ if (substr_count($vars, "&") > 0) {
+ $qs = explode("&", $vars);
+ foreach ($qs as $q) {
+ $new .= self::_parm_encode($q);
+ }
+ $new = rtrim($new, "&");
+ } else {
+ $new = rtrim(self::_parm_encode($vars), "&");
+ }
+
+ return $new;
+ }
+
+ /**
+ * Purpose to auto create url based on if short url is needed
+ * @param string $project - Which folder
+ * @param string $route
+ * @param string $method
+ * @param type $vars
+ * @retval string url
+ */
+ public static function get_url(string $route, string $method, $vars = ''): string {
+ $route = ltrim($route, "/");
+
+ if (is_array($vars)) {
+ $vars = http_build_query($vars);
+ } elseif (is_string($vars) && !empty($vars)) {
+ $vars = self::safe_url($vars);
+ }
+
+ if (bootstrap\configure::get('CodeHydrater', 'short_url') === true) {
+ $vars = (!empty($vars)) ? "?{$vars}" : '';
+ return PROJECT_BASE_REF . "/{$route}/{$method}.html{$vars}";
+ } else {
+ $vars = (!empty($vars)) ? "&{$vars}" : '';
+ return PROJECT_BASE_REF . "?route={$route}&m={$method}{$vars}";
+ }
+ }
+
+ /**
+ * Purpose to auto create API url based on if short url is needed
+ * @param string $route
+ * @param string $method
+ * @param type $vars
+ * @retval string API url
+ */
+ public static function get_api_url(string $route, string $method, $vars = ''): string {
+ $route = ltrim($route, "/");
+
+ if (is_array($vars)) {
+ $vars = http_build_query($vars);
+ } elseif (is_string($vars) && !empty($vars)) {
+ $vars = self::safe_url($vars);
+ }
+
+ if (bootstrap\configure::get("CodeHydrater", 'short_url') === true) {
+ $vars = (!empty($vars)) ? "?{$vars}" : '?x=0';
+ return PROJECT_BASE_REF . "/api/{$route}/{$method}{$vars}&api=true";
+ } else {
+ $vars = (!empty($vars)) ? "&{$vars}" : '';
+ return PROJECT_BASE_REF . "?route={$route}&m={$method}&code=/api/{$vars}&api=true";
+ }
+ }
+
+ /**
+ * Checks if current URI is from an API call
+ * @retval bool
+ */
+ public static function is_api(): bool {
+ $uri = bootstrap\siteHelper::request_uri();
+ if (strlen($uri) > 2) {
+ return ((substr_count($uri, "/api/") == 1 && self::get_var('api') == 'true') || (substr_count($uri, "/api2/") == 1 && self::get_var('api2') == 'true')) ? true : false;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * site http://php.net/manual/en/function.base64-encode.php
+ */
+ public static function base64url_encode(string $data): string {
+ return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+ }
+
+ public static function base64url_decode(string $data): string {
+ //return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
+ return base64_decode( strtr( $data, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $data )) % 4 ));
+ }
+
+ public static function get_globals(array $skip = ['route', 'm'], array $only_these = []): string {
+ $the_request = '';
+
+ $getArray = filter_input_array(INPUT_GET) ?? [];
+ $postArray = filter_input_array(INPUT_POST) ?? [];
+ $my_requests = array_merge($getArray, $postArray);
+
+ foreach ($my_requests as $key => $value) {
+ if (count($skip) && in_array($key, $skip)) {
+ continue;
+ }
+
+ $safe_key = filter_var(trim($key), FILTER_UNSAFE_RAW);
+ $url_key = self::encode_url_var($safe_key);
+
+ $data = filter_var($key);
+ $safe_value = filter_var(trim($data), FILTER_UNSAFE_RAW);
+ $url_value = self::encode_url_var($safe_value);
+
+ if (in_array($key, $only_these) || !count($only_these)) {
+ $the_request .= '&' . $url_key . '=' . $url_value;
+ }
+ }
+
+ return strip_tags($the_request);
+ }
+
+}
diff --git a/src/classes/page_not_found.php b/src/classes/page_not_found.php
new file mode 100644
index 0000000..8ba5e41
--- /dev/null
+++ b/src/classes/page_not_found.php
@@ -0,0 +1,69 @@
+ 1 ) {
+ $err .= $argv[1];
+ }
+
+ $exists = bootstrap\registry::get('di')->exists('log');
+ if ($exists) {
+ $log = bootstrap\registry::get('di')->get_service('log', ['CLI_404s']);
+ $log->write($err);
+ }
+
+ echo $err;
+
+ exit(1);
+ }
+
+ /**
+ * Displays 404 Page not Found
+ */
+ public static function error404(): void {
+ if (consoleApp::is_cli()) {
+ self::error404_cli();
+ } else {
+ $use_api = misc::is_api();
+ }
+
+ if ($use_api === true) {
+ self::api_method_not_found();
+ }
+ // Show 404, Page Not Found Error Page!
+ if (bootstrap\requires::secure_include('404_page.php', bootstrap\UseDir::ONERROR) === false) {
+ $loaded = bootstrap\requires::secure_include('views/on_error/404_page.php', bootstrap\UseDir::FRAMEWORK);
+ if ($loaded === false) {
+ echo "404 Page Not Found!
";
+ }
+ }
+ exit(1);
+ }
+
+ /**
+ * API Method was not found do API 400 Error
+ */
+ private static function api_method_not_found(): void {
+ $status = 400; // Bad Request
+ $bad_request = api::BAD_REQUEST;
+ api::error(array('response' => $bad_request, 'code' => $status, 'reason' => 'Command not found'));
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/router.php b/src/classes/router.php
new file mode 100644
index 0000000..606f42e
--- /dev/null
+++ b/src/classes/router.php
@@ -0,0 +1,487 @@
+ '(\d+)', // Any Number
+ 's' => '(\w+)', // Any Word
+ 'locale' => '(sk|en)->en'
+ ];
+
+ /**
+ * Init new self instance
+ */
+ public static function init() {
+ self::$instance = new self();
+ }
+
+ /**
+ * Assign name to route
+ *
+ * @param $name
+ */
+ public function name($name)
+ {
+ $route = self::$routes[self::$last];
+ unset(self::$routes[self::$last]);
+ self::$routes[self::$name . $name] = $route;
+ }
+
+ /**
+ * Add custom shortcut
+ * shortcut with default value -> $regex = (val1|val2)->val1;
+ *
+ * @param $shortcut
+ * @param $regex
+ */
+ public static function addShortcut($shortcut, $regex)
+ {
+ self::$shortcuts[$shortcut] = $regex;
+ }
+
+ /**
+ * Get link from route name and params
+ *
+ * @param $route
+ * @param array $params
+ * @param $absolute
+ * @return null
+ */
+ public static function link($route, $params = [], $absolute = true)
+ {
+ if (!isset(self::$routes[$route])) return null;
+ $route = self::$routes[$route];
+
+ $link = "";
+ foreach ($route['params'] as $key => $param) {
+ if (isset($param['real'])) {
+ $link .= $param['pattern'] . '/';
+ } else if (isset($params[$param['name']])) {
+ // Chcek if param has default
+ if (isset($param['default']) && $param['default'] !== $params[$param['name']]) {
+ $link .= $params[$param['name']] . '/';
+ }
+ }
+ }
+
+ // Add absolute path
+ if ($absolute) {
+ $link = self::$URL . $link;
+ }
+
+ // Cut slash at the end
+ return rtrim($link, '/');
+ }
+
+ /**
+ * Redirect to specific route or defined location
+ *
+ * @param $route
+ * @param array $params
+ */
+ public static function redirect($route, $params = [])
+ {
+ if (isset(self::$routes[$route])) {
+ $route = self::link($route, $params);
+ }
+ header('Location: ' . $route, true);
+ die();
+ }
+
+ /**
+ * Create prefixed routes
+ *
+ * @param $prefix
+ * @param $callback
+ * @param string $name
+ * @param array $doNotIncludeInParams
+ */
+ public static function prefix($prefix, $callback, $name = '', $doNotIncludeInParams = [])
+ {
+ self::$prefix = $prefix;
+ self::$name = $name;
+ self::$doNotIncludeInParams = $doNotIncludeInParams;
+ call_user_func($callback);
+ self::$prefix = null;
+ self::$name = null;
+ self::$doNotIncludeInParams = [];
+ }
+
+ /**
+ * Create route
+ *
+ * @param $route
+ * @param $action
+ * @param array $method
+ * @return Router
+ */
+ public static function route($route, $action, $method = ["POST", "GET"])
+ {
+ $prefix = self::$prefix;
+ if (empty($route) && !empty(self::$prefix)) {
+ $prefix = rtrim(self::$prefix, '/');
+ }
+
+
+ $explodedRoute = explode("/", $prefix . $route);
+ $params = [];
+ $pattern = "";
+
+ // create route
+ foreach ($explodedRoute as $key => $r) {
+ if (strpos($r, '}?') !== false) {
+ $r = self::dynamic($r, $params, true);
+ $pattern = substr($pattern, 0, -1);
+ $dyn = true;
+ } else if (strpos($r, '}') !== false) {
+ $r = self::dynamic($r, $params, false);
+ $pattern = substr($pattern, 0, -1);
+ $dyn = true;
+ } else {
+ $params[] = [
+ 'pattern' => $r,
+ 'real' => false
+ ];
+ }
+ $pattern .= ($key == 0 && !isset($dyn) ? '/' : '') . $r . '/';
+ }
+
+ // Create pattern
+ $pattern = substr($pattern, 0, -1) . '/?';
+ $pattern = '~^' . str_replace('/', '\/', $pattern) . '$~';
+
+ // Save data to static property
+ $name = uniqid();
+ self::$routes[$name] = [
+ 'route' => $route,
+ 'pattern' => $pattern,
+ 'params' => $params,
+ 'action' => $action,
+ 'method' => $method,
+ 'doNotIncludeInParams' => self::$doNotIncludeInParams
+ ];
+ // Set last added
+ self::$last = $name;
+
+ // return self instance
+ if (self::$instance == null) {
+ self::init();
+ }
+ return self::$instance;
+ }
+
+ public static function get($route, $action) {
+ return self::route($route, $action, ['GET']);
+ }
+
+ public static function post($route, $action) {
+ return self::route($route, $action, ['POST']);
+ }
+
+ public static function put($route, $action) {
+ return self::route($route, $action, ['PUT']);
+ }
+
+ public static function delete($route, $action) {
+ return self::route($route, $action, ['DELETE']);
+ }
+
+ public static function any($route, $action) {
+ return self::route($route, $action, ['GET', 'POST', 'PUT', 'DELETE']);
+ }
+
+ /**
+ * Create all routes for resource (CRUD)
+ *
+ * @param $route
+ * @param $controller
+ * @param string $name
+ * @param string $shortcut
+ */
+ public static function resource($route, $controller, $name = null, $shortcut = 's')
+ {
+ self::$name = self::$name . $name;
+
+ self::get($route . '/{page::paginator}?', $controller . "@index")->name('index');
+ self::get($route . "/create", $controller . "@create")->name('create');
+ self::post($route, $controller . "@store")->name('store');
+ self::get($route . "/{id::" . $shortcut . "}", $controller . "@show")->name('show');
+ self::get($route . "/{id::" . $shortcut . "}/edit", $controller . "@edit")->name('edit');
+ self::put($route . "/{id::" . $shortcut . "}", $controller . "@update")->name('update');
+ self::delete($route . "/{id::" . $shortcut . "}", $controller . "@destroy")->name('destroy');
+ self::get($route . "/{id::" . $shortcut . "}/delete", $controller . "@destroy")->name('destroy_');
+
+ self::$name = str_replace($name, '', self::$name);
+ }
+
+
+ /**
+ * Handle dynamic parameter in route
+ *
+ * @param string $route
+ * @param array $params
+ * @param boolean $optional
+ * @return mixed
+ */
+ private static function dynamic($route, &$params, $optional = false)
+ {
+ $shortcut = self::rules($route);
+
+ $name = str_replace('{', '', $route);
+ if (!$optional) {
+ $name = str_replace('}', '', $name);
+ } else {
+ $name = str_replace('}?', '', $name);
+ }
+
+ if (strpos($name, '::')) {
+ $name = substr($name, 0, strpos($name, "::"));
+ }
+
+ if (array_search($name, array_column($params, 'name')) === false) {
+
+ $pattern = str_replace('(', '(/', $shortcut['shortcut']);
+ $pattern = str_replace('|', '|/', $pattern);
+
+ // If is optional add ? at the end of pattern
+ if ($optional) {
+ $pattern .= '?';
+ }
+
+ $params[] = [
+ 'name' => $name,
+ 'pattern' => $pattern,
+ 'default' => $shortcut['default']
+ ];
+ } else {
+ throw new \Exception('Parameter with name ' . $name . ' has been already defined');
+ }
+ return $pattern;
+ }
+
+ /**
+ * Dynamic route rules
+ *
+ * @param $route
+ * @return mixed
+ */
+ private static function rules($route)
+ {
+ if (preg_match('~::(.*?)}~', $route, $match)) {
+ list(, $shortcut) = $match;
+ if (isset(self::$shortcuts[$shortcut])) {
+ // Try to get default value from shortcut
+ $default = false;
+ $shortcut = self::$shortcuts[$shortcut];
+ if (preg_match('~->(.*?)$~', $shortcut, $match)) {
+ $default = $match[1];
+ $shortcut = str_replace($match[0], '', $shortcut);
+ }
+ // Return shortcut and default value
+ return [
+ 'shortcut' => $shortcut,
+ 'default' => $default
+ ];
+ }
+ }
+ return [
+ 'shortcut' => self::$shortcuts['s'],
+ 'default' => false
+ ];
+ }
+
+ /**
+ * get_all_routes -> Added by Robert Strutts to auto load routes
+ * Namespace must start with Project
+ * Needs a routes folder, then
+ * either an routes.php or test_routes.php files must exists
+ * with a method called get!
+ */
+ private static function get_all_routes(bool $testing = false) {
+ $route_name = (consoleApp::is_cli()) ? "cli_routes" : "routes";
+ $routes = ($testing) ? "test_routes" : $route_name;
+ $routes_class = "\\Project\\routes\\{$routes}";
+
+ if (class_exists($routes_class)) {
+ if (method_exists($routes_class, "get")) {
+ $callback = "{$routes_class}::get";
+ call_user_func($callback);
+ }
+ }
+ }
+
+ /**
+ * Execute router
+ * @todo add Kernel/Requests...
+ */
+ public static function execute() {
+ $ROOT = bootstrap\siteHelper::get_root();
+ $request_uri = bootstrap\siteHelper::get_uri();
+ $request_method = bootstrap\siteHelper::get_method();
+ $testing = bootstrap\siteHelper::get_testing();
+
+ // Generate request and absolute path
+ self::generateURL($ROOT, $request_uri);
+ // Fecth all Routes from the Project
+ self::get_all_routes($testing);
+
+ // Get query string
+ $request = explode('?', $request_uri);
+ $queryParams = [];
+ if (isset($request[1])) {
+ $queryParams = $request[1];
+ parse_str($queryParams, $queryParams);
+ self::$queryParams = $queryParams;
+ }
+
+ // Modify request
+ $request = '/' . trim(self::$REQUEST, '/');
+
+ // Find route
+ foreach (self::$routes as $routeKey => $route) {
+ $post_method = \tts\misc::post_var("_method");
+ $matchMethod = in_array($request_method, $route['method']) || ($post_method !== null
+ && in_array($post_method, $route['method']));
+ if (preg_match($route['pattern'], $request, $match) && $matchMethod) {
+
+ // Default variables
+ $explodedRequest = explode('/', ltrim($request, '/'));
+ $routeParams = $route['params'];
+
+ // Match request params with params in array - static params
+ foreach ($explodedRequest as $key => $value) {
+ foreach ($routeParams as $k => $routeParam) {
+ // Go to the next request part
+ if (isset($routeParam['real']) && $routeParam['pattern'] == $value) {
+ unset($routeParams[$k]);
+ unset($explodedRequest[$key]);
+ break;
+ }
+ }
+ }
+
+ $number = "(/\d+)";
+ $params = [];
+ // Match request params with params in array - dynamic params
+ foreach ($routeParams as $k => $routeParam) {
+ $value = $explodedRequest[$k] ?? false;
+ $default = $routeParam['default'] ?? true;
+ $pattern = $routeParam['pattern'] ?? "";
+ $wild = str_contains($pattern, "?");
+ if (! $wild && ($value === false && $default)) {
+ $params[$routeParam['name']] = null;
+ } else if ($wild && ($value === false && ! $default)) {
+ // Let the default function params be used by not setting anything here!
+ } else if (preg_match('~' . $pattern . '~', '/' . $value, $match)) {
+ if (str_contains($pattern, $number)) {
+ $params[$routeParam['name']] = (int) $value;
+ } else {
+ $params[$routeParam['name']] = $value;
+ }
+ unset($routeParams[$k]);
+ }
+ }
+
+ // Merge with query params
+ self::$params = array_merge($params, $queryParams);
+
+ // Setup default route and url
+ self::$route = $route;
+
+ foreach ($params as $key => $value) {
+ if (in_array($key, $route['doNotIncludeInParams']) && !is_numeric($key)) {
+ unset($params[$key]);
+ }
+ }
+
+ // Call action
+ if (is_callable($route['action'])) {
+ $returned = call_user_func_array($route['action'], $params);
+ return ["found"=> true, "returned"=> $returned];
+ } else if (strpos($route['action'], '@') !== false) {
+
+ // call controller
+ list($controller, $method) = explode('@', $route['action']);
+
+ // init new controller
+ $controller = new $controller;
+
+ // Check if class has parent
+ $parentControllers = class_parents($controller);
+ if (!empty($parentControllers)) {
+ end($parentControllers);
+ $parentController = $parentControllers[key($parentControllers)];
+ $parentController = new $parentController;
+
+ // Add properties to parent class
+ foreach ($params as $key => $value) {
+ $parentController::$params[$key] = $value;
+ }
+ }
+
+ //Call method
+ if (method_exists($controller, $method)) {
+ $returned = call_user_func_array([$controller, $method], $params);
+ return ["found"=> true, "returned"=> $returned];
+ }
+ }
+ }
+ }
+ return ["found"=>false];
+ }
+
+ /**
+ * Generate URL
+ *
+ * @param $ROOT
+ */
+ private static function generateURL(string $ROOT, string $request_uri)
+ {
+ $https = bootstrap\saferIO::get_clean_server_var('HTTPS');
+ $baseLink = ($https === 'on') ? "https" : "http";
+
+ $server_name = bootstrap\saferIO::get_clean_server_var('SERVER_NAME');
+ $baseLink .= "://" . $server_name;
+
+ $port = bootstrap\saferIO::get_clean_server_var('SERVER_PORT');
+ $baseLink .= ($port !== '80') ? ':' . $port : ':';
+
+ $baseRequest = '';
+
+ $request = $request_uri;
+ foreach (explode('/', $ROOT) as $key => $value) {
+ if ($value == '') continue;
+ if (preg_match('~/' . $value . '~', $request)) {
+ $baseRequest .= $value . '/';
+ }
+ $request = preg_replace('~/' . $value . '~', '', $request, 1);
+ }
+
+ self::$URL = $baseLink . '/' . $baseRequest;
+ self::$REQUEST = explode('?', $request)[0];
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/security.php b/src/classes/security.php
new file mode 100644
index 0000000..f29849a
--- /dev/null
+++ b/src/classes/security.php
@@ -0,0 +1,264 @@
+pdo->prepare(trim($sql));
+ if (\bs_tts\common::get_count($bind) > 0) {
+ $exec = $pdostmt->execute($bind);
+ } else {
+ $exec = $pdostmt->execute();
+ }
+ return $pdostmt->rowCount();
+ }
+
+ public function run_select($sql) {
+ $pdostmt = $this->pdo->prepare(trim($sql));
+ $exec = $pdostmt->execute();
+ return $pdostmt->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+ private function describe_table(string $table) {
+ $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
+ $tick_table = (strrpos($table, "`") === false) ? "`{$table}`" : $table;
+ if ($driver == 'sqlite') {
+ $sql = "PRAGMA table_info({$tick_table});";
+ $key = "name";
+ } elseif ($driver == 'mysql') {
+ $sql = "DESCRIBE {$tick_table};";
+ $key = "Field";
+ } elseif ($driver == 'pgsql') {
+ $sql = "SELECT column_name FROM information_schema.columns WHERE table_name = '{$table}';";
+ $key = "column_name";
+ } else {
+ return false;
+ }
+ return ['sql'=>$sql, 'key'=>$key];
+ }
+
+ public function get_fields(string $table) {
+ $describe = $this->describe_table($table);
+ if (false !== ($list = $this->run_select($describe['sql']))) {
+ $fields = array();
+ if (count($list) == 0) {
+ return array();
+ }
+ foreach ($list as $record) {
+ $fields[] = $record[$describe['key']];
+ }
+ return $fields;
+ }
+ return array();
+ }
+
+ private function filter(string $table, array $info): array {
+ $describe = $this->describe_table($table);
+ if ($describe === false) {
+ return array_keys($info);
+ }
+ if (false !== ($list = $this->run_select($describe['sql']))) {
+ $fields = array();
+ foreach ($list as $record) {
+ $fields[] = $record[$describe['key']];
+ }
+ return array_values(array_intersect($fields, array_keys($info)));
+ }
+ return array();
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/traits/database/validation.php b/src/classes/traits/database/validation.php
new file mode 100644
index 0000000..9740422
--- /dev/null
+++ b/src/classes/traits/database/validation.php
@@ -0,0 +1,204 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+namespace tts\traits\database;
+
+trait validation {
+
+ /**
+ * Validate current class members
+ * @retval bool true valid, false failed tests
+ */
+ public function validate_mysql(): bool {
+ $tbl = (\bs_tts\common::is_string_found($this->table, "`")) ? $this->table : "`{$this->table}`";
+ foreach ($this->members as $field => $value) {
+ if ($field == $this->primary_key) {
+ continue;
+ }
+
+ $validation_field = $this->get_vaildation_member($field);
+ if (isset($validation_field['native_type']) && isset($validation_field['len'])) {
+ $type = strtoupper($validation_field['native_type']);
+ $len = intval($validation_field['len']);
+ $meta = $validation_field;
+ } else {
+ $meta = $this->get_MySQL_meta_field($field, $this->table);
+ $type = (isset($meta['native_type']) ? $meta['native_type'] : '');
+ $len = $meta['len'];
+ }
+
+ switch ($type) { //This should be all uppercase input.
+ case 'SHORT': //Small INT
+ case 'INT24': //MED INT
+ case 'LONGLONG': //BIG INT or SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
+ case 'LONG': // Integers
+ if (!preg_match('/^[0-9]*$/', $value)) {
+ $this->do_verr("Failed Validation: NOT a digit {$type} {$field}");
+ return false;
+ } // Does not allow decimal numbers!!
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'FLOAT':
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ if (!is_float($value)) {
+ $this->do_verr("Failed Validation: NOT a float {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'NEWDECIMAL':
+ $prec = intval($meta['precision']);
+ $maxlen = $len - $prec;
+ if (!\bs_tts\common::is_string_found($value, '.')) {
+ $this->do_verr("Failed Validation: No Decimal Found in field {$field}");
+ return false;
+ }
+ $x = explode('.', $value);
+ if (strlen($x[0]) >= ($maxlen - 1) || strlen($x[1]) > $prec) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'DOUBLE':
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ if (!is_double($value)) {
+ $this->do_verr("Failed Validation: NOT a double {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'BLOB': // Text
+ if ($len == '4294967295' || $len == '16777215')
+ continue 2; //Too Big to process, 16777215 MEDIUMTEXT
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'VAR_STRING': // VARCHAR or VARBINARY
+ case 'STRING': //CHAR or BINARY
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ break;
+ case 'DATE':
+ if (!$this->is_valid_mysql_date($value)) {
+ $this->do_verr("Failed Validation: invalid date in {$field}");
+ return false;
+ }
+ break;
+ case 'TIME':
+ if (!$this->is_valid_mysql_time($value)) {
+ $this->do_verr("Failed Validation: invalid time in {$field}");
+ return false;
+ }
+ break;
+ case 'TIMESTAMP':
+ case 'DATETIME':
+ if (strlen($value) > $len) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ if (!$this->is_valid_mysql_timestamp($value)) {
+ $this->do_verr("Failed Validation: invalid timestamp in {$field}");
+ return false;
+ }
+ break;
+ default: //TINYINT, Bit, Bool, or Year is the default for no meta data
+ //if (!is_Digits($value)) return false; //This fails so its commented out.
+ if ($len == 3) { // Tiny INT
+ if (intval($value) > 255) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ if (intval($value) < -127) {
+ $this->do_verr("Failed Validation: too short {$type} {$field}");
+ return false;
+ }
+ } elseif ($len == 1) { // Bit or Bool
+ if (intval($value) > 9) {
+ $this->do_verr("Failed Validation: too long {$type} {$field}");
+ return false;
+ }
+ if (intval($value) < 0) {
+ $this->do_verr("Failed Validation: too short {$type} {$field}");
+ return false;
+ }
+ }
+ break;
+ }
+ }
+ return true;
+ }
+
+ public function get_MySQL_meta_field(string $field, string $table): array {
+ $query = "SELECT `{$field}` FROM {$table} LIMIT 1";
+ $pdo_stmt = $this->pdo->prepare($query);
+ $pdo_stmt->execute();
+ return $pdo_stmt->getColumnMeta(0);
+ }
+
+ /**
+ * Check if valid timestamp/datetime
+ * @param type $Str
+ * @return type
+ */
+ public function is_valid_mysql_timestamp(string $Str): bool {
+ $Stamp = strtotime($Str);
+ $Month = date('m', $Stamp);
+ $Day = date('d', $Stamp);
+ $Year = date('Y', $Stamp);
+
+ return checkdate($Month, $Day, $Year);
+ }
+
+ public function is_valid_mysql_date(string $str): bool {
+ $date_parts = explode('-', $str);
+ if (\main_tts\common::count($date_parts) != 3)
+ return false;
+ if ((strlen($date_parts[0]) != 4) || (!is_numeric($date_parts[0])))
+ return false;
+ if ((strlen($date_parts[1]) != 2) || (!is_numeric($date_parts[1])))
+ return false;
+ if ((strlen($date_parts[2]) != 2) || (!is_numeric($date_parts[2])))
+ return false;
+ if (!checkdate($date_parts[1], $date_parts[2], $date_parts[0]))
+ return false;
+ return true;
+ }
+
+ public function is_valid_mysql_time(string $str): bool {
+ return (strtotime($str) === false) ? false : true;
+ }
+
+ /**
+ * Helper function for validate mysql
+ * Will echo if not live error message
+ * If enabled will also log the error.
+ * @param string $msg error message
+ */
+ private function do_verr(string $msg): void {
+ $this->error_message = $msg;
+ $exists = \main_tts\registry::get('di')->exists('log');
+ if ($exists && \main_tts\configure::get('database', 'log_validation_errors') === true) {
+ $log = \main_tts\registry::get('di')->get_service('log', ['validation_errors']);
+ $log->write($msg);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/classes/traits/security/csrf_token_functions.php b/src/classes/traits/security/csrf_token_functions.php
new file mode 100644
index 0000000..3740352
--- /dev/null
+++ b/src/classes/traits/security/csrf_token_functions.php
@@ -0,0 +1,87 @@
+";
+ }
+
+ /**
+ * Check if POST data CSRF Token is Valid
+ * @return bool is valid
+ */
+ public static function csrf_token_is_valid(): bool {
+ $is_csrf = filter_has_var(INPUT_POST, 'csrf_token');
+ if ($is_csrf) {
+ $user_token = \tts\misc::post_var('csrf_token');
+ $stored_token = $_SESSION['csrf_token'] ?? '';
+ if (empty($stored_token)) {
+ return false;
+ }
+ return \tts\misc::compair_it($user_token, $stored_token);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Optional check to see if token is also recent
+ * @return bool
+ */
+ public static function csrf_token_is_recent(): bool {
+ $max_elapsed = intval(\main_tts\configure::get(
+ 'security',
+ 'max_token_age'
+ ));
+ if ($max_elapsed < 30) {
+ $max_elapsed = 60 * 60 * 24; // 1 day
+ }
+
+ if (isset($_SESSION['csrf_token_time'])) {
+ $stored_time = $_SESSION['csrf_token_time'];
+ return ($stored_time + $max_elapsed) >= time();
+ } else {
+ // Remove expired token
+ self::destroy_csrf_token();
+ return false;
+ }
+ }
+
+}
diff --git a/src/classes/traits/security/session_hijacking_functions.php b/src/classes/traits/security/session_hijacking_functions.php
new file mode 100644
index 0000000..4b5a400
--- /dev/null
+++ b/src/classes/traits/security/session_hijacking_functions.php
@@ -0,0 +1,149 @@
+= time()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+// Should the session be considered valid?
+ public static function is_session_valid() {
+ $check_ip = true;
+ $check_user_agent = true;
+ $check_last_login = true;
+
+ if ($check_ip && !self::request_ip_matches_session()) {
+ return false;
+ }
+ if ($check_user_agent && !self::request_user_agent_matches_session()) {
+ return false;
+ }
+ if ($check_last_login && !self::last_login_is_recent()) {
+ return false;
+ }
+ return true;
+ }
+
+// If session is not valid, end and redirect to login page.
+ public static function confirm_session_is_valid() {
+ if (!self::is_session_valid()) {
+ self::end_session();
+ // Note that header redirection requires output buffering
+ // to be turned on or requires nothing has been output
+ // (not even whitespace).
+ header("Location: login.php");
+ exit;
+ }
+ }
+
+// Is user logged in already?
+ public static function is_logged_in() {
+ return (isset($_SESSION['logged_in']) && $_SESSION['logged_in']);
+ }
+
+// If user is not logged in, end and redirect to login page.
+ public static function confirm_user_logged_in() {
+ if (!self::is_logged_in()) {
+ self::end_session();
+ // Note that header redirection requires output buffering
+ // to be turned on or requires nothing has been output
+ // (not even whitespace).
+ header("Location: login.php");
+ exit;
+ }
+ }
+
+// Actions to preform after every successful login
+ public static function after_successful_login() {
+ // Regenerate session ID to invalidate the old one.
+ // Super important to prevent session hijacking/fixation.
+ session_regenerate_id();
+
+ $_SESSION['logged_in'] = true;
+
+ // Save these values in the session, even when checks aren't enabled
+ $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
+ $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
+ $_SESSION['last_login'] = time();
+ }
+
+// Actions to preform after every successful logout
+ public static function after_successful_logout() {
+ $_SESSION['logged_in'] = false;
+ self::end_session();
+ }
+
+// Actions to preform before giving access to any
+// access-restricted page.
+ public static function before_every_protected_page() {
+ self::confirm_user_logged_in();
+ self::confirm_session_is_valid();
+ }
+
+}
diff --git a/src/classes/validator.php b/src/classes/validator.php
new file mode 100644
index 0000000..b450030
--- /dev/null
+++ b/src/classes/validator.php
@@ -0,0 +1,234 @@
+ 'Please enter the %s',
+ 'email' => 'The %s is not a valid email address',
+ 'less_than' => 'The %s number must be less than %d',
+ 'greater_than' => 'The %s number must be greater than %d',
+ 'number_range' => 'The %s number must be in range of %d to %d',
+ 'min' => 'The %s must have at least %s characters',
+ 'max' => 'The %s must have at most %s characters',
+ 'between' => 'The %s must have between %d and %d characters',
+ 'same' => 'The %s must match with %s',
+ 'alphanumeric' => 'The %s should have only letters and numbers',
+ 'secure' => 'The %s must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and one special character',
+ 'valid_email_domain' => 'The %s email address is not active',
+ 'valid_domain' => 'The %s domain name is not active',
+ ];
+
+ private static function make_arrays(array $data, $field): array {
+ $dataset = [];
+ if (isset($data[$field])) {
+ if (bootstrap\common::get_count($data[$field])) {
+ foreach($data[$field] as $the_data) {
+ $dataset[] = $the_data;
+ }
+ } else {
+ $dataset[] = $data[$field];
+ }
+ } else {
+ $dataset[] = null; // If field is null, force null set
+ }
+ return $dataset;
+ }
+
+ public static function validate(
+ array $data,
+ array $fields,
+ array $messages = []
+ ): array {
+ // Split the array by a separator, trim each element
+ // and return the array
+ $split = fn($str, $separator) => array_map('trim', explode($separator, $str));
+
+ // get the message rules
+ $rule_messages = array_filter($messages, fn($message) => is_string($message));
+ // overwrite the default message
+ $validation_errors = array_merge(self::DEFAULT_VALIDATION_ERRORS, $rule_messages);
+
+ $errors = [];
+
+ foreach ($fields as $field => $option) {
+ foreach(self::make_arrays($data, $field) as $index=>$v) {
+ $data[$field] = $v; // Force update on arrays
+
+ $rules = $split($option, '|');
+
+ foreach ($rules as $rule) {
+ // get rule name params
+ $params = [];
+ // if the rule has parameters e.g., min: 1
+ if (strpos($rule, ':')) {
+ [$rule_name, $param_str] = $split($rule, ':');
+ $params = $split($param_str, ',');
+ } else {
+ $rule_name = trim($rule);
+ }
+ // by convention, the callback should be is_ e.g.,is_required
+ $fn = 'is_' . $rule_name;
+
+ $callable = self::class . "::{$fn}";
+ if (is_callable($callable)) {
+ $pass = $callable($data, $field, ...$params);
+ if (!$pass) {
+ // get the error message for a specific field and rule if exists
+ // otherwise get the error message from the $validation_errors
+ $errors[$field] = sprintf(
+ $messages[$field][$rule_name] ?? $validation_errors[$rule_name],
+ $field,
+ ...$params
+ );
+ }
+ }
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ private static function is_required(array $data, string $field): bool {
+ if (isset($data[$field])) {
+ if (bootstrap\common::get_count($data[$field])) {
+ return false; // Should not be an array here
+ }
+ if (is_string($data[$field])) {
+ return (trim($data[$field]) !== '');
+ }
+ if (is_int($data[$field])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static function is_email(array $data, string $field): bool {
+ if (empty($data[$field])) {
+ return true;
+ }
+ return filter_var($data[$field], FILTER_VALIDATE_EMAIL);
+ }
+
+ private static function is_min(array $data, string $field, string $min): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ return mb_strlen($data[$field]) >= intval($min);
+ }
+
+ private static function is_max(array $data, string $field, string $max): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ return mb_strlen($data[$field]) <= intval($max);
+ }
+
+ private static function is_greater_than(array $data, string $field, string $min): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ return intval($data[$field]) > intval($min);
+ }
+
+ private static function is_less_than(array $data, string $field, string $max): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ return intval($data[$field]) < intval($max);
+ }
+
+ private static function is_number_range(array $data, string $field, string $min, string $max): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ $no = intval($data[$field]);
+ return $no >= intval($min) && $no <= intval($max);
+ }
+
+ private static function is_between(array $data, string $field, string $min, string $max): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ $len = mb_strlen($data[$field]);
+ return $len >= intval($min) && $len <= intval($max);
+ }
+
+ private static function is_same(array $data, string $field, string $other): bool {
+ if (isset($data[$field]) && isset($data[$other])) {
+ return $data[$field] === $data[$other];
+ }
+
+ return false;
+ }
+
+ private static function is_alphanumeric(array $data, string $field): bool {
+ if (!isset($data[$field])) {
+ return true;
+ }
+
+ return ctype_alnum($data[$field]);
+ }
+
+ private static function is_secure(array $data, string $field): bool {
+ if (!isset($data[$field])) {
+ return false;
+ }
+ // Is 8 to 64 CHRs
+ $pattern = "#.*^(?=.{8,64})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$#";
+ return preg_match($pattern, $data[$field]);
+ }
+
+ private static function is_valid_email_domain(array $data, string $field): bool {
+ if (!isset($data[$field])) {
+ return false;
+ }
+
+ $domain = ltrim(stristr($data[$field], '@'), '@') . '.';
+ return checkdnsrr($domain, 'MX');
+ }
+
+ private static function is_valid_domain(array $data, string $field): bool {
+ if (!isset($data[$field])) {
+ return false;
+ }
+
+ return checkdnsrr($data[$field], 'A')
+ || checkdnsrr($data[$field], 'AAAA')
+ || checkdnsrr($data[$field], 'CNAME');
+ }
+
+}
+
+/*
+$data = ['email'=>'jim@aol.com'];
+$fields = ['email' => 'required | email'];
+
+$errors = \CodeHydrater\validator::validate($data, $fields);
+print_r($errors);
+*/
+
+/*
+ * 'firstname' => 'required | max:255',
+ 'lastname' => 'required | max: 255',
+ 'address' => 'required | min: 10 | max:255',
+ 'zipcode' => 'between: 5,6',
+ 'username' => 'required | alphanumeric | between: 3,255',
+ 'email' => 'required | email | valid_email_domain',
+ */
\ No newline at end of file
diff --git a/src/views/on_error/404_page.php b/src/views/on_error/404_page.php
new file mode 100644
index 0000000..c87aeac
--- /dev/null
+++ b/src/views/on_error/404_page.php
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+ 404 Page not found!
+
+
+
+
+

+
+
Our apologies for the temporary inconvenience.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DEV ERROR!
+
+
+
+
+
+
+ = $local->page_output; ?>
+
+
+
\ No newline at end of file
diff --git a/src/views/on_error/prod_error.php b/src/views/on_error/prod_error.php
new file mode 100644
index 0000000..e0d49c3
--- /dev/null
+++ b/src/views/on_error/prod_error.php
@@ -0,0 +1,43 @@
+
+ * @copyright Copyright (c) 2022, Robert Strutts.
+ * @license MIT
+ */
+
+define('PRODUCTION', 600);
+define('MAINTENACE', 3600); // 1 hour = 3600 seconds
+define('RETRY_AFTER', PRODUCTION);
+
+if(! headers_sent()) {
+ header('HTTP/1.1 503 Service Temporarily Unavailable');
+ header('Status: 503 Service Temporarily Unavailable');
+ header('Retry-After: ' . RETRY_AFTER);
+}
+?>
+
+
+
+
+
+
+
+
+
+ Sorry, we had an error...
+
+
+
+
+
+ Sorry, we had an error...
+ We apologize for any inconvenience this may cause.
+
+
+
+