From 8b021101674217a573ad8416550d32e6fda52b7d Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 24 Jul 2025 19:46:02 -0400 Subject: [PATCH] init 4 --- docs/TODO.md | 8 +- src/bootstrap/errors.php | 158 ++++++++++++++++++++++++++++-- src/bootstrap/main.php | 31 +++++- src/bootstrap/site_helper.php | 12 ++- src/classes/app.php | 2 +- src/classes/database/model.php | 4 +- src/classes/database/paginate.php | 31 ++---- src/classes/html_document.php | 2 +- src/classes/misc.php | 7 +- src/classes/router.php | 2 +- src/classes/security.php | 19 ++++ src/classes/view.php | 4 +- 12 files changed, 225 insertions(+), 55 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 9ffdd58..e6102d1 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -15,7 +15,7 @@ [x] → Macros - [ ] → Routes + [x] → Routes [x] → Controllers @@ -31,11 +31,11 @@ [ ] → Main Project Tempates - [o] → 404 Pages/Dev/Prod Errors + [x] → 404 Pages/Dev/Prod Errors [x] → CLI Detect - [ ] → Paginator + [x] → Paginator [x] → Safer I/O @@ -53,7 +53,7 @@ [x] → Error Handler - [ ] → CSRF Tokens + [x] → CSRF Tokens [x] → Password Hashing/Verify diff --git a/src/bootstrap/errors.php b/src/bootstrap/errors.php index df6f8f0..250b46a 100644 --- a/src/bootstrap/errors.php +++ b/src/bootstrap/errors.php @@ -11,9 +11,6 @@ if (! defined('BaseDir')) { define('LOG_FILE', BaseDir . '/protected/logs/error_log.txt'); } -if (! defined('ENVIRONMENT')) { - define('ENVIRONMENT', 'production'); // 'production' or 'development' -} /** * Format message with appropriate colors based on environment */ @@ -82,11 +79,18 @@ set_error_handler(function($errno, $errstr, $errfile, $errline) { file_put_contents(LOG_FILE, $logMessage, FILE_APPEND); } // Display in development environment - if (ENVIRONMENT === 'development') { + if (fallback_dev()) { echo formatMessage($displayMessage, $errorCategory); + // Prevent PHP's default error handler + return true; } - // Prevent PHP's default error handler + if (is_on_error_page() === true) { + if (fallback_requires('prod_error.php') === false) { + fallback_requires('views/on_error/prod_error.php', true); + } + exit(1); // Prevent HTML Looping!!! + } return true; }); @@ -101,7 +105,8 @@ set_exception_handler(function($e) { if (LOG_FILE !== false) { file_put_contents(LOG_FILE, $logMessage, FILE_APPEND); } - if (ENVIRONMENT === 'development') { + // Are we developing...? if so, show details on error + if (fallback_dev()) { echo formatMessage($displayMessage, 'error'); } else { // In production, show user-friendly message @@ -124,12 +129,151 @@ register_shutdown_function(function() { if (LOG_FILE !== false) { file_put_contents(LOG_FILE, $logMessage, FILE_APPEND); } - if (ENVIRONMENT === 'development') { + // If developing, show error + if (fallback_dev()) { echo formatMessage($displayMessage, 'error'); } } }); +function fallback_is_live(): bool { + return is_live(); // from main.php +} + +/** + * Displays Error page + * @param type $ex error message + */ +function broken_error($ex = ''): void { + if (fallback_is_cli()) { + echo $ex; + exit(1); + } + $use_api = fallback_is_api(); + + if ($use_api === true) { + $internal_error = "An unexpected error occured."; + $status_code = 500; + fallback_json_error($internal_error, $status_code); + } + + if (fallback_is_live()) { + // Try really Hard to display Prod Error Page + $exists = fallback_requires('prod_error.php'); + if (! $exists) { + $exists = fallback_requires('views/on_error/prod_error.php', true); + } + if ($exists === false) { + echo "

Sorry, we had an error...

We apologize for any inconvenience this may cause.

"; + } + } else { + if ($ex === "") { + echo "

Unknown Issue...

"; + } else { + echo $ex; + } + echo "

Error Page

Please go back to another page...

"; + } +} + +function fallback_json_error(string $data, int $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; +} + +function fallback_is_api(): bool { + $uri = fallback_get_uri(); + if (preg_match('~/api(\d*)/~', $uri)) { + return true; + } + return false; +} + +function fallback_is_cli() { + if (defined('STDIN')) { + return true; + } + + if (php_sapi_name() === 'cli') { + return true; + } + + if (array_key_exists('SHELL', $_ENV)) { + return true; + } + + if (!isset($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT'])) { + return true; + } + + if (!array_key_exists('REQUEST_METHOD', $_SERVER)) { + return true; + } + + return false; +} + +function fallback_get_uri(): ?string { + return strtok($_SERVER['REQUEST_URI']); +} + +function fallback_dev(): bool { + if (! defined("ENVIRONMENT")) { + return false; // Force Live on unknown state + } + if (ENVIRONMENT === 'development') { + return true; + } + return false; +} + +function fallback_requires(string $file, bool $fw = false): bool { + $exists = method_exists("\CodeHydrater\bootstrap\requires", "secure_include"); + if ($exists) { + if ($fw === false) { + $path = UseDir::ONERROR; + } else { + $path = UseDir::FRAMEWORK; + } + $good = \CodeHydrater\bootstrap\requires::secure_include($file, $path); + return ($good === false) ? false : true; // handle contents vs. failure = false + } else { + if ($fw === false) { + $view = CodeHydrater_PROJECT . "views/on_error/" . $file; + } else { + $view = CodeHydrater_FRAMEWORK . $file; + } + if (file_exists($view)) { + include $view; + return true; + } + } + return false; // You have failed me for the last time! LOL +} + +/** + * Check if already on Error Page + * @return bool + */ +function is_on_error_page(): bool { + if (fallback_is_cli()) { + exit(1); + } else { + return (stripos(fallback_get_uri(), "error.html") !== false); + } +} + + // 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"); diff --git a/src/bootstrap/main.php b/src/bootstrap/main.php index c9e46c4..4b43e77 100644 --- a/src/bootstrap/main.php +++ b/src/bootstrap/main.php @@ -40,10 +40,17 @@ if (site_helper::get_testing() === false) { } final class errors { + private static $is_live = true; // Fall Back Value if Config Not Found private static $handle_global_errors = true; private static $handle_shutdown_errors = true; private static $handle_exceptions = true; - + + public static function set_live_mode(bool $is_live): void { + self::$is_live = $is_live; + } + public static function get_live_mode(): bool { + return self::$is_live; + } public static function set_handle_shutdown_errors(bool $do_handle_errors): void { self::$handle_shutdown_errors = $do_handle_errors; } @@ -274,10 +281,6 @@ if (extension_loaded('mbstring')) { 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/auto_loader.php'; @@ -290,6 +293,24 @@ registry::get('loader')->add_namespace("Project", CodeHydrater_PROJECT); load_all::init(CodeHydrater_PROJECT); +if (! defined('ENVIRONMENT')) { + if (is_live() === false) { + define('ENVIRONMENT', 'development'); + } else { + define('ENVIRONMENT', 'production'); + } +} + +function is_live(): bool { + if (configure::has('CodeHydrater', 'live')) { + $live = configure::get('CodeHydrater', 'live'); + } + if ($live === null) { + $live = errors::get_live_mode(); + } + return (bool) $live; +} + $returned_route = \CodeHydrater\router::execute(); if ($returned_route["found"] === false) { $app = new \CodeHydrater\app(); diff --git a/src/bootstrap/site_helper.php b/src/bootstrap/site_helper.php index 4eb574b..9f00554 100644 --- a/src/bootstrap/site_helper.php +++ b/src/bootstrap/site_helper.php @@ -40,14 +40,14 @@ final class site_helper { public static function set_allowed_Private_IPs(string|array $IP_addresses): void { if (is_array($IP_addresses)) { foreach($IP_addresses as $IP) { - $s_ip = \CodeHydrater\security::get_valid_ip($IP); + $s_ip = \CodeHydrater\security::is_private_or_local_IP_simple($IP); if ($s_ip === false) { continue; } self::$Private_IPs_allowed[] = $IP; } } elseif (is_string($IP_addresses)) { - $s_ip = \CodeHydrater\security::get_valid_ip($IP); + $s_ip = \CodeHydrater\security::is_private_or_local_IP_simple($IP); if ($s_ip === false) { return; } @@ -79,7 +79,7 @@ final class site_helper { } public static function remote_not_allowed_force_live(): bool { - return (! self::is_allowed()); + return (self::is_allowed() === false) ? true : false; } public static function is_allowed(): bool { @@ -87,7 +87,10 @@ final class site_helper { if (in_array($remote_ip, self::$Public_IPs_allowed)) { return true; } - if (self::is_server_name_a_private_domain() && in_array($remote_ip, self::$Private_IPs_allowed)) { + if (in_array($remote_ip, self::$Private_IPs_allowed)) { + return true; + } + if (self::is_server_name_a_private_domain()) { return true; } return false; @@ -243,5 +246,6 @@ final class site_helper { define('PROJECT_BASE_REF', SITE_URL); define("BROWSER", self::get_clean_server_var('HTTP_USER_AGENT')); define("ASSETS_DIR", "/public/assets/"); + define("PROJECT_ASSETS_BASE_REF", ASSETS_BASE_REF); } } \ No newline at end of file diff --git a/src/classes/app.php b/src/classes/app.php index 913bc62..5371170 100644 --- a/src/classes/app.php +++ b/src/classes/app.php @@ -44,7 +44,7 @@ class app { public function __construct() { $full_route = bootstrap\site_helper::get_route(); $no_hmtl = str_replace(".html", "", $full_route); - + // Find the Route $route = $this->get_first_chunks($no_hmtl); diff --git a/src/classes/database/model.php b/src/classes/database/model.php index 5b1d27e..decac92 100644 --- a/src/classes/database/model.php +++ b/src/classes/database/model.php @@ -44,7 +44,7 @@ class model { * JSON API inform AJAX that save failed */ public function save_failed($msg = 'Save Failed!'): void { - \CodeHydrater\api::error(array('code' => 500, 'success' => false, 'error' => $msg, 'responce' => \tts\api::INTERNAL_ERROR)); + \CodeHydrater\api::error(array('code' => 500, 'success' => false, 'error' => $msg, 'responce' => \CodeHydrater\api::INTERNAL_ERROR)); } public function dump_table_fields(string $table) { @@ -123,7 +123,7 @@ class model { return; // Bail if LIVE } if (isset($post['json'])) { - $globals = \CodeHydrater\bootstrap\safer_io::get_json_post_data(); + $globals = SafeIO::get_json_post_data(); } else { $json_post = count($post); $globals = ($json_post) ? $post : filter_input_array(INPUT_POST) ?? []; diff --git a/src/classes/database/paginate.php b/src/classes/database/paginate.php index f1cd04a..6efdfc4 100644 --- a/src/classes/database/paginate.php +++ b/src/classes/database/paginate.php @@ -7,7 +7,7 @@ declare(strict_types=1); * @link https://code.tutsplus.com/tutorials/how-to-paginate-data-with-php--net-2928 */ -namespace tts\database; +namespace CodeHydrater\database; class paginate { @@ -210,6 +210,12 @@ class paginate { . "{$end_label}\n"; } + public function css_style(): string { + return ".pagination { display: inline-block; } +.pagination a { color: black; float: left; padding: 8px 16px;text-decoration: none; } +.pagination a.active { background-color: #4CAF50; color: white; } +.pagination a:hover:not(.active) { background-color: #ddd; }"; + } } /* @@ -229,27 +235,4 @@ class paginate { } echo $pag->create_links(); - */ - - -/* - * css - * -.pagination { - display: inline-block; -} - -.pagination a { - color: black; - float: left; - padding: 8px 16px; - text-decoration: none; -} - -.pagination a.active { - background-color: #4CAF50; - color: white; -} - -.pagination a:hover:not(.active) {background-color: #ddd;} */ \ No newline at end of file diff --git a/src/classes/html_document.php b/src/classes/html_document.php index 3c0b2e4..f70ef9a 100644 --- a/src/classes/html_document.php +++ b/src/classes/html_document.php @@ -360,7 +360,7 @@ class html_document { * Used by Template file to render HTML Meta data for Robots * @return string HTML Meta Robots */ - public function get_robots(): string { + public function get_robots(): ?string { return $this->robots; } diff --git a/src/classes/misc.php b/src/classes/misc.php index 72252f8..6ff5aa5 100644 --- a/src/classes/misc.php +++ b/src/classes/misc.php @@ -321,11 +321,10 @@ final class misc { */ public static function is_api(): bool { $uri = bootstrap\site_helper::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; + if (preg_match('~/api(\d*)/~', $uri)) { + return true; } + return false; } /** diff --git a/src/classes/router.php b/src/classes/router.php index 974bdec..52be4e6 100644 --- a/src/classes/router.php +++ b/src/classes/router.php @@ -362,7 +362,7 @@ class router // Find route foreach (self::$routes as $routeKey => $route) { - $post_method = \tts\misc::post_var("_method"); + $post_method = 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) { diff --git a/src/classes/security.php b/src/classes/security.php index 18c6a79..a8144ae 100644 --- a/src/classes/security.php +++ b/src/classes/security.php @@ -182,6 +182,25 @@ final class security { return crc32($_SESSION['user_id']); } + public static function is_private_or_local_IP_simple(string $ip): bool { + if (! self::get_valid_ip($ip)) { + return false; // Invalid + } + return ( + $ip === '::1' || // IPv6 localhost + preg_match('/^127\./', $ip) || // IPv4 localhost + preg_match('/^10\./', $ip) || // 10.0.0.0/8 + preg_match('/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $ip) || // 172.16.0.0/12 + preg_match('/^192\.168\./', $ip) || // 192.168.0.0/16 + preg_match('/^fd[0-9a-f]{2}:/i', $ip) // IPv6 ULA (fc00::/7) + ); + } + + /** + * Filter IP return good IP or False! + * @param string $ip + * @return string | false + */ public static function get_valid_ip(string $ip) { return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6)); } diff --git a/src/classes/view.php b/src/classes/view.php index 505a916..58dcf8f 100644 --- a/src/classes/view.php +++ b/src/classes/view.php @@ -192,7 +192,7 @@ final class view { if (count($this->files) > 0) { foreach ($this->files as $view_file) { if ($view_file['file_type'] == '.php') { - bootstrap\requires::secure_include($view_file['file'], $view_file['path'], $local, $this->vars); // Include the file + bootstrap\requires::secure_include($view_file['file'], bootstrap\UseDir::PROJECT, $local, $this->vars); // Include the file } else { $template_file = str_replace('.tpl', '', $view_file['file']); $this->tempalte_engine->parse_file($template_file); @@ -228,7 +228,7 @@ final class view { bootstrap\views::ob_start(); $saved_ob_level = ob_get_level(); $local->page_output = $page_output; - bootstrap\requires::secure_include($this->template, 'project', $local, $this->vars); + bootstrap\requires::secure_include($this->template, bootstrap\UseDir::PROJECT, $local, $this->vars); $this->clear_ob($saved_ob_level); $page_output = ob_get_clean(); }