From 2fae0452e9d03508d3f10dcd834f60ae423f7a43 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 24 Jul 2025 02:22:21 -0400 Subject: [PATCH] init2 --- .../hydraterlicense/makelicense.zep | 27 + docs/TODO.md | 33 +- src/bootstrap/autoLoader.php | 142 ++++ src/bootstrap/common.php | 296 +++++++++ src/bootstrap/errors.php | 121 ++++ src/bootstrap/loadAll.php | 45 ++ src/bootstrap/main.php | 297 +++++++++ src/bootstrap/requires.php | 194 ++++++ src/bootstrap/saferIO.php | 628 ++++++++++++++++++ src/bootstrap/siteHelper.php | 222 +++++++ src/classes/api.php | 354 ++++++++++ src/classes/app.php | 205 ++++++ src/classes/consoleApp.php | 47 ++ src/classes/enums/safer_io_enums.php | 88 +++ src/classes/exceptions/Bool_Exception.php | 14 + src/classes/exceptions/DB_Exception.php | 103 +++ src/classes/html.php | 130 ++++ src/classes/makeLicenseFiles.php | 31 + src/classes/memory_usage.php | 71 ++ src/classes/misc.php | 370 +++++++++++ src/classes/page_not_found.php | 69 ++ src/classes/router.php | 487 ++++++++++++++ src/classes/security.php | 264 ++++++++ src/classes/traits/database/run_sql.php | 73 ++ src/classes/traits/database/validation.php | 204 ++++++ .../traits/security/csrf_token_functions.php | 87 +++ .../security/session_hijacking_functions.php | 149 +++++ src/classes/validator.php | 234 +++++++ src/views/on_error/404_page.php | 52 ++ src/views/on_error/dev_error.php | 23 + src/views/on_error/prod_error.php | 43 ++ 31 files changed, 5079 insertions(+), 24 deletions(-) create mode 100644 src/bootstrap/autoLoader.php create mode 100644 src/bootstrap/common.php create mode 100644 src/bootstrap/errors.php create mode 100644 src/bootstrap/loadAll.php create mode 100644 src/bootstrap/main.php create mode 100644 src/bootstrap/requires.php create mode 100644 src/bootstrap/saferIO.php create mode 100644 src/bootstrap/siteHelper.php create mode 100644 src/classes/api.php create mode 100644 src/classes/app.php create mode 100644 src/classes/consoleApp.php create mode 100644 src/classes/enums/safer_io_enums.php create mode 100644 src/classes/exceptions/Bool_Exception.php create mode 100644 src/classes/exceptions/DB_Exception.php create mode 100644 src/classes/html.php create mode 100644 src/classes/makeLicenseFiles.php create mode 100644 src/classes/memory_usage.php create mode 100644 src/classes/misc.php create mode 100644 src/classes/page_not_found.php create mode 100644 src/classes/router.php create mode 100644 src/classes/security.php create mode 100644 src/classes/traits/database/run_sql.php create mode 100644 src/classes/traits/database/validation.php create mode 100644 src/classes/traits/security/csrf_token_functions.php create mode 100644 src/classes/traits/security/session_hijacking_functions.php create mode 100644 src/classes/validator.php create mode 100644 src/views/on_error/404_page.php create mode 100644 src/views/on_error/dev_error.php create mode 100644 src/views/on_error/prod_error.php 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{$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{$nl}"; + } + echo "\t{$nl}"; + } + echo "
{$field}
{$cell}
{$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{$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{$nl}"; + } + echo "\t{$nl}"; + } + echo "
{$field}
{$cell}
{$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! + + + +
+ Page not found. +

404 Page not found!

+

Our apologies for the temporary inconvenience.

+
+ + + + + + + + + + + + + + + DEV ERROR! + + + + +
+
+ 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.

+ + + +