diff --git a/documents/folders.txt b/documents/folders.txt index cdfcc98..4f8904d 100644 --- a/documents/folders.txt +++ b/documents/folders.txt @@ -63,10 +63,11 @@ tts_framework/src │   │   │   ├── crypto.php (Newer sodium_crypto) │   │   │   ├── password_storage.php (Hash and Verify Hashed passwords) │   │   │   └── sodium_storage.php (Secure Cookies, Sessions, etc...) -│   │   ├── sessions (@todo Fix session destroy to erase old sessions) +│   │   ├── sessions +│   │   │   ├── cookie_sessions.php │   │   │   ├── file_sessions.php │   │   │   └── redis_sessions.php -│   │   ├── sessions.php (@todo Fix security to encrypt/decrypt sessions) +│   │   ├── sessions.php │   │   ├── simple_rest.php (demo REST API helper) │   │   └── twilio.php (Loads Twilio Vendor files into Name Space) │   ├── session_management.php (Starts PHP secure Sessions) @@ -83,12 +84,9 @@ tts_framework/src │ ├── validator.php (validates HTML Forms) │   └── view.php (Loads view files from common folders you defined) ├── main.inc.php (Bootstraps App, sets configure, registry, di, and name-spaces) -├── templates -│   └── dev_error.php (When NOT Live, show Exceptions/Errors) └── views - ├── 404.php (Default 404 Page Not Found Page/Image) - ├── default - │   └── broken.php (Debug Trace) - └── errors.php (when Live, this: Sorry, we had an error... Page is used) + ├── 404_page.php (Default 404 Page Not Found Page/Image) +    ├── dev_error.php (When NOT Live, show Exceptions/Errors) + └── prod_errors.php (when Live, this: Sorry, we had an error... Page is used) -~72 files +~73 files diff --git a/src/bootstrap/errors.php b/src/bootstrap/errors.php index 35d50ef..390b78d 100644 --- a/src/bootstrap/errors.php +++ b/src/bootstrap/errors.php @@ -33,20 +33,18 @@ function tts_broken_error($ex = ''): void { } if (\main_tts\is_live()) { - $resource = \main_tts\configure::get('tts', 'error_page'); - $exists = \bs_tts\requires::secure_include('views/errors', $resource); // Show Broken Page + if (\bs_tts\requires::secure_include('prod_error.php', 'on_error') === false) { + $exists = \bs_tts\requires::secure_include('views/on_error/prod_error.php', 'tts'); // Show Broken Page + } if ($exists === false) { echo "

Sorry, we had an error...

We apologize for any inconvenience this may cause.

"; } } else { - $view = new \tts\view(); - $view->set_view('broken', 'tts'); - $view->set('ex', $ex); - $view->fetch([]); + echo "

Error Page

Please go back to another page...

"; } } -function tts_mini_view(string $file, string $render, string $msg): string { +function tts_mini_view(string $file, string $msg): string { @ob_end_clean(); // Wipe all HTML content before this.... if (!headers_sent()) { header('Content-Type: text/html; charset=utf-8', true, 200); @@ -58,8 +56,9 @@ function tts_mini_view(string $file, string $render, string $msg): string { $mini = new stdClass(); $mini->page_output = $msg; - $file = 'templates/' . $file; - \bs_tts\requires::secure_include($file, $render, $mini); + if (\bs_tts\requires::secure_include($file, 'on_error', $mini) === false) { + \bs_tts\requires::secure_include("views/on_error/" . $file, 'tts', $mini); + } // If you really must close all of your output buffers except one, this'll do it: while (ob_get_level() > $saved_ob_level) { @@ -273,7 +272,7 @@ function tts_exception_handler(\Throwable $exception) { if (\main_tts\is_live()) { tts_global_error_handler(E_USER_ERROR, $err); } else { - echo tts_mini_view('dev_error', 'tts', $msg); + echo tts_mini_view('dev_error', $msg); } exit(1); } @@ -319,8 +318,8 @@ function tts_custom_error_checker(): void { $mini .= '
' . PHP_EOL; $mini .= "{$a_errors['message']}, in file: {$a_errors['file']}, on line #{$a_errors['line']}."; $mini .= '
' . PHP_EOL; - //$mini .= ''; - echo tts_mini_view('dev_error', 'tts', $mini); + + echo tts_mini_view('dev_error', $mini); exit(1); } } @@ -368,15 +367,19 @@ function tts_global_error_handler(int $errno = 0, string $errstr = '', string $e } if (\tts_is_on_error_page() === true) { - $resource = \main_tts\configure::get('tts', 'error_page'); - \bs_tts\requires::secure_include('views/errors', $resource); + if (\bs_tts\requires::secure_include('prod_error.php', 'on_error') === false) { + \bs_tts\requires::secure_include('views/on_error/prod_error.php', 'tts'); + } exit(1); // Prevent HTML Looping!!! } $http_response_code = 307; // 307 Temporary Redirect $prj = \main_tts\configure::get('tts', 'default_project'); - $ref = TTS_PROJECT_BASE_REF; - + $ref = (defined("TTS_PROJECT_BASE_REF")) ? TTS_PROJECT_BASE_REF : false; + if (empty($prj) || $ref === false) { + echo "

Sorry, we had an error...

We apologize for any inconvenience this may cause.

"; + exit(1); + } if (\bs_tts\common::is_string_found($ref, '/') === false) { $ref .= '/'; } diff --git a/src/bootstrap/requires.php b/src/bootstrap/requires.php index 42780d9..9968e75 100644 --- a/src/bootstrap/requires.php +++ b/src/bootstrap/requires.php @@ -105,7 +105,6 @@ final class requires { } else { $filtered_dir = rtrim(self::filter_dir_path($dir), '/') . '/'; $file_plus_dir = $filtered_dir . $filtered_file; - // $file_plus_dir = str_replace('_DS_', '/', $file_plus_dir); } $escaped_file = escapeshellcmd($file_plus_dir . $file_type); return self::get_PHP_Version_for_file($escaped_file); @@ -123,9 +122,9 @@ final class requires { $dir = match ($path) { "dir_fixed" => "", "framework", "tts" => \main_tts\TTS_FRAMEWORK, + "on_error" => \bs_tts\site_helper::get_root() . \bs_tts\site_helper::get_project() . "views/on_error/", default => \bs_tts\site_helper::get_root(), // project }; - $versioned_file = self::safer_file_exists($file, $dir); if ($versioned_file === false) { return false; diff --git a/src/bootstrap/safer_io.php b/src/bootstrap/safer_io.php index 0024ccb..6f895b5 100644 --- a/src/bootstrap/safer_io.php +++ b/src/bootstrap/safer_io.php @@ -85,7 +85,7 @@ final class safer_io { } // Allow anything to set_data_inputs is desired here - public static function set_data_input(string $var_name, $data_in): void { + 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; } diff --git a/src/classes/app.php b/src/classes/app.php index ab3b53f..1f0f5c0 100644 --- a/src/classes/app.php +++ b/src/classes/app.php @@ -166,6 +166,12 @@ class app { $call_class = "\\prj\\" . rtrim($project_folder, "/") . '\\' . $test . 'controllers\\' . $class; $controller = new $call_class(); + if ($method === "error" && str_contains($class, "app") && + method_exists($controller, $method) === false + ) { + tts_broken_error(); + return false; + } if ($use_api) { if (empty($method)) { diff --git a/src/classes/contracts/sessions_interface.php b/src/classes/contracts/sessions_interface.php index 8b69ee7..ea48219 100644 --- a/src/classes/contracts/sessions_interface.php +++ b/src/classes/contracts/sessions_interface.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace tts\contacts; - +use SessionHandlerInterface; interface sessions_interface { public function close(): bool; diff --git a/src/classes/page_not_found.php b/src/classes/page_not_found.php index fb2bdc6..673ef57 100644 --- a/src/classes/page_not_found.php +++ b/src/classes/page_not_found.php @@ -47,12 +47,13 @@ class page_not_found { if ($use_api === true) { self::api_method_not_found(); } - - $loaded = \bs_tts\requires::secure_include('views/404', 'tts'); // Show 404, Page Not Found Error Page! - if ($loaded === false) { - echo "

404 Page Not Found!

"; + // Show 404, Page Not Found Error Page! + if (\bs_tts\requires::secure_include('404_page.php', 'on_error') === false) { + $loaded = \bs_tts\requires::secure_include('views/on_error/404_page.php', 'tts'); + if ($loaded === false) { + echo "

404 Page Not Found!

"; + } } - exit(1); } diff --git a/src/classes/services/sessions/cookie_sessions.php b/src/classes/services/sessions/cookie_sessions.php new file mode 100644 index 0000000..2a68ce6 --- /dev/null +++ b/src/classes/services/sessions/cookie_sessions.php @@ -0,0 +1,114 @@ + + * @copyright Copyright (c) 2022, Robert Strutts. + * @license https://mit-license.org/ + */ + +class cookie_sessions_handler_exception extends \Exception {} + +class cookie_sessions implements \SessionHandlerInterface { + private static int $zlib_compression_level = 4; + public static string $cookie_domain; + public static string $cookie_name = 'SES'; + public static string $cookie_path = '/'; + public static bool $cookie_secure = true; + public static bool $cookie_HTTP_only = true; + private $enc; + + public static function set_zlib_compression_level(int $level): void { + self::$zlib_compression_level = ($level >= 0 && $level <= 9) ? + $level : 4; + } + + public function __construct($enc) { + self::$cookie_domain = $_SERVER['SERVER_NAME'] ?? ''; + $this->enc = $enc; + } + + private function encrypt(string & $data): void { + $data = $this->enc->encode("sess", $data); + } + + private function decrypt(string & $data): void { + try { + $data = $this->enc->decode("sess", $data); + } catch (\Exception $e) { + $data = false; // Maybe it has no data to decode + } + } + + private function compress(string & $data): void { + $data = gzdeflate($data, self::$zlib_compression_level); + if ($data === false) { + throw new \tts\services\sessions\cookie_sessions_handler_exception('Failed to compress session data'); + } + } + + private function decompress(string & $data): void { + $data = gzinflate($data); + } + + public function open($save_path, $session_name):bool { + return true; + } + + public function close(): bool { + return true; + } + + public function read($id): false|string { + $data = isset($_COOKIE[self::$cookie_name]) ? $_COOKIE[self::$cookie_name] : null; + if ($data === null) { + return ""; + } + + $data = base64_decode($data); + if ($data === false) { + return ""; + } + + $this->decrypt($data); + if ($data === false) { + return ""; + } + + $this->decompress($data); + return ($data !== false) ? $data : ''; + } + + public function write($id, $data): bool { + if (headers_sent($file, $line)) { + throw new \tts\services\sessions\cookie_sessions_handler_exception("Error headers were already sent by $file on line # $line "); + } + + $this->compress($data); + $this->encrypt($data); + $data = base64_encode($data); + /* + * Google Chrome - 4096 bytes confirming to RFC. + * If the data is over 4000 bytes, throw an exception + * as web browsers only support up to 4K of Cookie Data! + */ + if (strlen($data) > 4000) { + throw new \tts\services\sessions\cookie_sessions_handler_exception("Session data too big (over 4KB)"); + } + $cookie_lifetime = (int) ini_get('session.cookie_lifetime'); + + return setcookie(self::$cookie_name, $data, $cookie_lifetime, self::$cookie_path, self::$cookie_domain, self::$cookie_secure, self::$cookie_HTTP_only); + } + + public function destroy($id): bool { + $expiration_time = time() - 3600; + return setcookie(self::$cookie_name, "", $expiration_time, self::$cookie_path, self::$cookie_domain, self::$cookie_secure, self::$cookie_HTTP_only); + } + + public function gc($max_lifetime): int|false { + return 0; + } +} \ No newline at end of file diff --git a/src/classes/services/sessions/file_sessions.php b/src/classes/services/sessions/file_sessions.php index 13a96e7..8f7706b 100644 --- a/src/classes/services/sessions/file_sessions.php +++ b/src/classes/services/sessions/file_sessions.php @@ -12,7 +12,7 @@ namespace tts\services\sessions; class file_sessions implements \tts\contracts\sessions_interface { - private $savePath; + private $save_path; private function filter_id(string $id): string { if (\bs_tts\requires::is_valid_file($id)) { @@ -21,17 +21,16 @@ class file_sessions implements \tts\contracts\sessions_interface { throw new \Exception('Bad ID for session!'); } - public function open(string $savePath, string $sessionName): bool { - $safer_dir = \bs_tts\requires::safer_dir_exists($savePath); + public function open(string $save_path, string $session_name): bool { + $safer_dir = \bs_tts\requires::safer_dir_exists($save_path); if ($safer_dir === false) { return false; } - $this->savePath = $safer_dir; - if (!is_dir($this->savePath)) { - mkdir($this->savePath, 0777); + $this->save_path = $safer_dir; + if (!is_dir($this->save_path)) { + mkdir($this->save_path, 0777); } - return true; } @@ -39,33 +38,31 @@ class file_sessions implements \tts\contracts\sessions_interface { return true; } - public function read(string $id): string { + public function read(string $id): false|string { $safer_id = $this->filter_id($id); - return (string) @file_get_contents("{$this->savePath}/sess_{$safer_id}"); + return (string) @file_get_contents("{$this->save_path}/sess_{$safer_id}"); } public function write(string $id, string $data): bool { $safer_id = $this->filter_id($id); - return file_put_contents("{$this->savePath}/sess_{$safer_id}", $data) === false ? false : true; + return file_put_contents("{$this->save_path}/sess_{$safer_id}", $data) === false ? false : true; } public function destroy(string $id): bool { $safer_id = $this->filter_id($id); - $file = "{$this->savePath}/sess_{$safer_id}"; + $file = "{$this->save_path}/sess_{$safer_id}"; if (file_exists($file)) { unlink($file); } - return true; } - public function gc(int $maxlifetime): int { - foreach (glob("{$this->savePath}/sess_*") as $file) { - if (filemtime($file) + $maxlifetime < time() && file_exists($file)) { + public function gc(int $max_lifetime): int|false { + foreach (glob("{$this->save_path}/sess_*") as $file) { + if (filemtime($file) + $max_lifetime < time() && file_exists($file)) { unlink($file); } } - return true; } diff --git a/src/classes/services/sessions/redis_sessions.php b/src/classes/services/sessions/redis_sessions.php index aa52908..48e6f80 100644 --- a/src/classes/services/sessions/redis_sessions.php +++ b/src/classes/services/sessions/redis_sessions.php @@ -15,45 +15,38 @@ class redis_sessions implements \tts\contracts\sessions_interface { protected $db; protected $prefix; - public function __construct(PredisClient $db, $prefix = 'PHPSESSID:') { + public function __construct(PredisClient $db, $prefix = 'SESS:') { $this->db = $db; $this->prefix = $prefix; } - public function open($save_path, $session_name) { + public function open($save_path, $session_name): bool{ // No action necessary because connection is injected // in constructor and arguments are not applicable. } - public function close() { + public function close(): bool { $this->db = null; unset($this->db); } - public function read($id) { + public function read($id): false|string { $id = $this->prefix . $id; $sessData = $this->db->get($id); $this->db->expire($id, $this->ttl); return $sessData; } - public function write($id, $data) { + public function write($id, $data): bool { $id = $this->prefix . $id; $this->db->set($id, $data); $this->db->expire($id, $this->ttl); } - public function destroy($id) { + public function destroy($id): bool { $this->db->del($this->prefix . $id); } - public function gc($max_lifetime) { + public function gc($max_lifetime): int|false { // no action necessary because using EXPIRE } -} - -/* -$db = new PredisClient(); -$sessHandler = new redis_sessions($db); -session_set_save_handler($sessHandler); -session_start(); - */ \ No newline at end of file +} \ No newline at end of file diff --git a/src/classes/view.php b/src/classes/view.php index 45bb8cc..4ad1b07 100644 --- a/src/classes/view.php +++ b/src/classes/view.php @@ -38,18 +38,13 @@ final class view { /** * @todo Ignore render path tts, should go prj/,...,then tts */ - private function get_file(string $view_file, string $default, string $render_path = 'project'): string { + private function get_file(string $view_file, string $default): string { $file_ext = \bs_tts\common::get_string_right($view_file, 4); if (! \bs_tts\common::is_string_found($file_ext, '.')) { $view_file .= '.php'; } - if ($render_path === 'tts') { - $file = (empty($default)) ? "views/{$view_file}" : "views/{$default}/{$view_file}"; - $path = \main_tts\TTS_FRAMEWORK; - } else { - $file = (empty($default)) ? "{$this->project_dir}/views/{$view_file}" : "{$this->project_dir}/views/{$default}/{$view_file}"; - $path = \bs_tts\site_helper::get_root(); - } + $file = (empty($default)) ? "{$this->project_dir}/views/{$view_file}" : "{$this->project_dir}/views/{$default}/{$view_file}"; + $path = \bs_tts\site_helper::get_root(); $vf = $path . $file; if ( \bs_tts\requires::safer_file_exists($vf) !== false) { return $file; @@ -60,18 +55,18 @@ final class view { /** * Alias to set_view */ - public function include(string $file, string $render_path = 'project'): void { - $this->set_view($file, $render_path); + public function include(string $file): void { + $this->set_view($file); } /** * Alias to set_view */ - public function add_view(string $file, string $render_path = 'project'): void { - $this->set_view($file, $render_path); + public function add_view(string $file): void { + $this->set_view($file); } - private function find_view_path(string $view_file, string $render_path) { + private function find_view_path(string $view_file) { $found = false; $default_paths = \main_tts\configure::get('view_mode', 'default_paths'); @@ -84,7 +79,7 @@ final class view { } foreach ($default_paths as $default) { - $file = $this->get_file($view_file, $default, $render_path); + $file = $this->get_file($view_file, $default); if ( ! empty($file) ) { $found = true; break; @@ -93,11 +88,10 @@ final class view { return ($found) ? $file : false; } - private function find_template_path($tpl_file, $render_path) { + private function find_template_path($tpl_file) { $file = "{$this->project_dir}/views/includes/{$tpl_file}"; - $path = ($render_path === 'project') ? \bs_tts\site_helper::get_root() : \main_tts\TTS_FRAMEWORK; + $path = \bs_tts\site_helper::get_root(); $vf = $path . $file; - return \bs_tts\requires::safer_file_exists($vf); } @@ -107,7 +101,7 @@ final class view { * @param string $render_path * @throws Exception */ - public function set_view(string $view_file = null, string $render_path = 'project'): void { + public function set_view(string $view_file = null): void { if ($view_file === null) { return; } @@ -121,16 +115,16 @@ final class view { } if ($file_ext === '.php') { - $file = $this->find_view_path($view_file, $render_path); + $file = $this->find_view_path($view_file); } else { - $file = $this->find_template_path($view_file, $render_path); + $file = $this->find_template_path($view_file); } if ($file === false) { echo "No view file exists for: {$view_file}!"; throw new \Exception("View File does not exist: " . $view_file); } else { - $this->files[] = array('file'=>$file, 'path'=>$render_path, 'file_type'=>$file_ext); + $this->files[] = array('file'=>$file, 'path'=>"project", 'file_type'=>$file_ext); } } diff --git a/src/views/default/broken.php b/src/views/default/broken.php deleted file mode 100644 index 35830c3..0000000 --- a/src/views/default/broken.php +++ /dev/null @@ -1,7 +0,0 @@ - -
-
-
-
diff --git a/src/views/404.php b/src/views/on_error/404_page.php similarity index 100% rename from src/views/404.php rename to src/views/on_error/404_page.php diff --git a/src/templates/dev_error.php b/src/views/on_error/dev_error.php similarity index 100% rename from src/templates/dev_error.php rename to src/views/on_error/dev_error.php diff --git a/src/views/errors.php b/src/views/on_error/prod_error.php similarity index 100% rename from src/views/errors.php rename to src/views/on_error/prod_error.php