main
Robert 5 months ago
parent 3f91fc5272
commit 818aebc99c
  1. 6
      docs/TODO.md
  2. 2
      src/bootstrap/common.php
  3. 2
      src/classes/collection.php
  4. 29
      src/classes/database/model.php
  5. 12
      src/classes/dollars.php
  6. 16
      src/classes/enums/compression_method.php
  7. 48
      src/classes/gz_compression.php
  8. 16
      src/classes/lazy_collection.php
  9. 2
      src/classes/lazy_object.php
  10. 8
      src/classes/middleware.php
  11. 4
      src/classes/scalar.php
  12. 355
      src/classes/services/encryption.php
  13. 137
      src/classes/services/sessions/cookie_session_handler.php
  14. 181
      src/classes/services/sessions/file_session_handler.php
  15. 175
      src/classes/services/sessions/redis_session_handler.php
  16. 87
      src/classes/session_management.php
  17. 4
      src/classes/traits/database/run_sql.php
  18. 14
      src/classes/traits/database/validation.php

@ -5,7 +5,7 @@
[x] → AEBootLoader and Generator (Encrypted Features and Fall Backs)
[ ] → Encrypted Sessions
[x] → Encrypted/Compressed Sessions
[ ] → Kernels
@ -19,7 +19,7 @@
[x] → Controllers
[ ] → Models
[.] → Models
[ ] → Views (Twig/Liquid/PHP)
@ -29,7 +29,7 @@
[x] → HTML Document
[ ] → Main Project Tempates
[x] → Main Project Tempates
[x] → 404 Pages/Dev/Prod Errors

@ -251,7 +251,7 @@ final class common {
* @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) {
if (\CodeHydrater\bootstrap\configure::get('security', 'show_dumps') !== true) {
return;
}
if (!is_object($var)) {

@ -5,7 +5,7 @@ namespace CodeHydrater;
/**
* @template TItem
*/
final readonly class Collection {
final readonly class collection {
/**
* @param array<int, TItem> $items
*/

@ -353,5 +353,34 @@ class model {
return self::successful_save;
}
/**
* This FN requires SSL connection to the database.
* So, the KEY does not get exposed!!!!
*/
public function mysql_aes_encode(string $data, string $key, bool $bind = true) {
$safe_text = addslashes($data);
$safe_key = addslashes($key);
$parm = ($bind) ? ":{$safe_text}" : "'{$safe_text}'";
$ret = "HEX(AES_ENCRYPT($parm},'{$safe_key}'))";
\CodeHydrater\bootstrap\common::wipe($key);
\CodeHydrater\bootstrap\common::wipe($safe_key);
\CodeHydrater\bootstrap\common::wipe($data);
\CodeHydrater\bootstrap\common::wipe($safe_text);
\CodeHydrater\bootstrap\common::wipe($parm);
return $ret;
}
/**
* This FN requires SSL connection to the database.
* So, the KEY does not get exposed!!!!
*/
public function mysql_aes_decode(string $field_name, string $key, bool $add_as = false) {
$safe_field = addslashes($field_name);
$safe_key = addslashes($key);
$as = ($add_as === true) ? " AS `{$safe_field}`" : "";
return "CAST(AES_DECRYPT(UNHEX(`{$safe_field}`),'{$safe_key}') AS char){$as}";
}
/* End Model */
}

@ -8,21 +8,21 @@ namespace CodeHydrater;
use BcMath\Number;
class Dollars {
private function moneyAsFloat(Number $money): float {
class dollars {
private function money_as_float(Number $money): float {
// Remove any existing currency symbols and thousands separators
$t = preg_replace('/[^\d.-]/', '', $money);
return (float)$t;
}
public function formatAsUSD(Number $money): string {
return '$' . number_format($this->moneyAsFloat($money), 2, '.', ',');
public function format_as_USD(Number $money): string {
return '$' . number_format($this->money_as_float($money), 2, '.', ',');
}
public function intlUSD(Number $money): string|false {
public function intl_USD(Number $money): string|false {
if (class_exists('NumberFormatter')) {
$formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
return $formatter->formatCurrency($this->moneyAsFloat($money), 'USD');
return $formatter->formatCurrency($this->money_as_float($money), 'USD');
}
return false;
}

@ -0,0 +1,16 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts <Bob_586@Yahoo.com>
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace CodeHydrater\enums;
enum compression_method: string {
case GZIP = 'gzip';
case DEFLATE = 'deflate';
case ZLIB = 'zlib';
}

@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts <Bob_586@Yahoo.com>
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace CodeHydrater;
use \CodeHydrater\enums\compression_method as Method;
class gz_compression {
public function __construct(
public Method $method = Method::DEFLATE,
public int $compression_level = 4,
public bool $use_compression = true
) {}
public function compress(string $data): string|false {
if ($this->use_compression === false) {
return false;
}
$level = ($this->compression_level < 10 && $this->compression_level > 0) ? $this->compression_level : 4;
$c = match($this->method) {
Method::DEFLATE => gzdeflate($data, $level),
Method::GZIP => gzencode($data, $level),
Method::ZLIB => gzcompress($data, $level),
};
return base64_encode($c);
}
public function decompress(string $compressed_data): string|false {
if ($this->use_compression === false) {
return false;
}
$d = base64_decode($compressed_data);
return match($this->method) {
Method::DEFLATE => gzinflate($d),
Method::GZIP => gzdecode($d),
Method::ZLIB => gzuncompress($d),
};
}
}

@ -6,7 +6,7 @@ namespace CodeHydrater;
* @template TValue
* @implements \IteratorAggregate<TKey, TValue>
*/
class LazyCollection implements \IteratorAggregate
class lazy_collection implements \IteratorAggregate
{
/** @var \Closure|self|array<TKey, TValue>|null */
protected $source;
@ -21,23 +21,23 @@ class LazyCollection implements \IteratorAggregate
} elseif (is_null($source)) {
$this->source = static::empty();
} else {
$this->source = $this->getArrayableItems($source);
$this->source = $this->get_arrayable_items($source);
}
}
/**
* @return \Traversable<TKey, TValue>
*/
public function getIterator(): \Traversable
public function get_iterator(): \Traversable
{
return $this->makeIterator($this->source);
return $this->make_iterator($this->source);
}
/**
* @param \Closure(): iterable<TKey, TValue>|self<TKey, TValue>|array<TKey, TValue> $source
* @return \Traversable<TKey, TValue>
*/
protected function makeIterator($source)
protected function make_iterator($source)
{
if ($source instanceof \Closure) {
$source = $source();
@ -65,9 +65,9 @@ class LazyCollection implements \IteratorAggregate
* @param callable(TValue, TKey): bool $callback
* @return self<TKey, TValue>
*/
public function each(callable $callback): LazyCollection
public function each(callable $callback): lazy_collection
{
foreach ($this->getIterator() as $key => $value) {
foreach ($this->get_iterator() as $key => $value) {
if ($callback($value, $key) === false) {
break;
}
@ -80,7 +80,7 @@ class LazyCollection implements \IteratorAggregate
* @param mixed $items
* @return array<TKey, TValue>
*/
protected function getArrayableItems($items): array
protected function get_arrayable_items($items): array
{
if (is_array($items)) {
return $items;

@ -2,7 +2,7 @@
namespace CodeHydrater;
class LazyObject {
class lazy_object {
public function __construct(public string $className) {}
public function proxy(mixed ...$args): Object {

@ -2,12 +2,12 @@
namespace CodeHydrater;
interface Middleware
interface middleware
{
public function handle(array $request, \Closure $next, mixed ...$parameters): mixed;
}
class Pipeline
class pipeline
{
protected array $middleware = [];
protected $request;
@ -27,7 +27,7 @@ class Pipeline
public function then(\Closure $destination): array
{
$pipeline = array_reduce(
array_reverse($this->parseMiddleware($this->middleware)),
array_reverse($this->parse_middleware($this->middleware)),
$this->carry(),
function ($request) use ($destination) {
return $destination($request);
@ -37,7 +37,7 @@ class Pipeline
return $pipeline($this->request);
}
protected function parseMiddleware(array $middleware): array
protected function parse_middleware(array $middleware): array
{
return array_map(function ($m) {
return is_string($m) ? explode(':', $m, 2) : $m;

@ -2,8 +2,8 @@
namespace CodeHydrater;
class Scalar {
public function intoString(mixed $name): string {
class scalar {
public function into_string(mixed $name): string {
if (is_scalar($name) || (is_object($name) && method_exists($name, '__toString'))) {
return (string)$name;
} else {

@ -0,0 +1,355 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts <Robert@TryingToScale.com>
* @copyright Copyright (c) 2022, Robert Strutts.
* @license MIT
*
* This file is for non-sensitive data like session data
*/
namespace CodeHydrater\services;
/*
* var_dump($enc->list_ssl_methods());
* var_dump($enc->list_hashes());
*/
final class encryption {
const iterations = 81952; // should be over 80K. The number of internal iterations to perform for the derivation.
const length = 64; // The length of the output string. If raw_output is TRUE this corresponds to the byte-length of the derived key, if raw_output is FALSE this corresponds to twice the byte-length of the derived key (as every byte of the key is returned as two hexits).
const raw = true; // When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.
const key_bytes = 32; // 32 bytes = 256 bits, 64 bytes = 512 bits, 16 bytes = 128 bits encryption key
private $random_engine;
private $binary = false;
private $url_encode = false;
private $method = 'AES-256-CBC';
private $default_hash = 'sha256'; // should be sha256 or higher
private $_iterations = self::iterations, $_length=self::length, $_raw=self::raw, $_key_bytes=self::key_bytes;
public function __construct(private string $key) {
$this->random_engine = new \CodeHydrater\random_engine();
}
/*
* xxHash is an extremely fast hashing algorithm
* that is not designed for cryptographic purposes, but
* provides excellent randomness and dispersion of output,
* and uniqueness of to minimize collisions.
*
Algorithm PHP implementation speed (GB/s)
xxh3 15.19
xxh128 14.78
crc32c 14.12
xxh64 13.32
murmur3f 8.87
xxh32 7.47
sha2-256 0.25
sha1-160 0.70
md5-128 0.77
*/
public function weak_quick_hash(string $data, int $speed): string {
$version = (float) phpversion();
if ($version >= 8.1) {
return match($speed) {
1 => hash('xxh3', $data), // Fastest
2 => hash('xxh128', $data),
3 => hash('crc32c', $data),
4 => hash('xxh64', $data),
5 => hash('murmur3f', $data),
6 => hash('xxh32', $data),
7 => hash('sha256', $data), // Slowest
8 => hash('sha1', $data),
9 => hash('md5', $data),
default => crc32($data)
};
} else {
return crc32($data);
}
}
public function change_security_level(string $level): bool {
switch (strtolower($level)) {
/**
* About blowfish ->
* Designers: Bruce Schneier
* First published: 1993
* Successors: Twofish
* Key sizes: 32–448 bits
* Block sizes: 64 bits
* Structure: Feistel network
* Rounds: 16
*/
/*
* In cryptography, Twofish is a symmetric key block cipher
* with a block size of 128 bits and key sizes up to 256 bits.
* Twofish was slightly slower than Rijndael for 128-bit keys,
* but somewhat faster for 256-bit keys.
*/
/*
* AES is the main block cipher in use today, standardized by NIST. Camellia is a Japanese standardized cipher. ChaCha is a fast stream cipher specified by Bernstein and incorporated into TLS with support from Google.
* Serpent and Twofish were AES last round candidates that didn't make it. Serpent is not that fast, and Twofish is relatively fast but not compared to AES when hardware acceleration is used. Both are block ciphers that are really not needed as long as we deem AES to be secure.
* Threefish was mainly designed for the Skein hash function. This tweakable block cipher is not used much. It Skein had been chosen as SHA-3 then it would have stood a better chance. For now an authenticated form of Keccak would make more sense.
* So yeah, two standardized ciphers and a fast stream ciphers are supported, none of which are broken. There is no reason to include the also-ran's and a cipher made specifically for a hash function which was also not standardized.
*/
/**
* lighting, blaze, and quick are not for sensitive data,
* it is good for making sure data was not tampered with
* like encrypted sessions. Where as, good, normal,
* and paranoid are for cookies, DB storage, etc...
*/
case 'lighting': // 0.0006 Seconds
$this->set_loops(98); // Very very fast
$this->set_length(64);
$this->set_key_bytes(16);
$this->method = 'AES-128-CBC';
$this->default_hash = 'sha256';
return true;
case 'blaze': // 0.0109 Seconds
$this->set_loops(1843);
$this->set_length(64);
$this->set_key_bytes(32);
$this->method = 'AES-128-CBC';
$this->default_hash = 'sha256';
return true;
case 'quick': // 0.0167 Seconds
$this->set_loops(2843);
$this->set_length(64);
$this->set_key_bytes(32);
$this->method = 'AES-192-CBC';
$this->default_hash = 'sha256';
return true;
case 'good': // 0.0732 Seconds
$this->set_loops(11952);
$this->set_length(64);
$this->set_key_bytes(32); // 32B or 256b, standard key size
$this->method = 'AES-256-CBC';
$this->default_hash = 'sha256';
return true;
case 'normal': // 0.4901 Seconds
$this->set_loops(81952); // slow
$this->set_length(64);
$this->set_key_bytes(32);
$this->method = 'AES-256-CBC';
$this->default_hash = 'sha256';
return true;
case 'paranoid': // 0.6167 Seconds
case 'max':
case 'high':
case 'slow-secure': // Very slow
$this->set_loops(82952);
$this->set_length(128); // extended length to make it work
$this->set_key_bytes(64); // extended key size for 512 hash
$this->method = 'AES-256-CBC';
$this->default_hash = 'sha512';
return true;
}
return false;
}
public function set_loops(int $iterations=self::iterations): void {
$this->_iterations = $iterations;
}
public function set_length(int $length=self::length): void {
$this->_length = $length;
}
public function set_raw_output(bool $output_raw=self::raw): void {
$this->_raw = $output_raw;
}
public function set_key_bytes(int $key_bytes=self::key_bytes): void {
$this->_key_bytes = $key_bytes;
}
public function list_ssl_methods(): array {
$ciphers = openssl_get_cipher_methods();
//ECB mode should be avoided
$ciphers = array_filter( $ciphers, function($n) { return stripos($n,"ecb")===FALSE; } );
//At least as early as Aug 2016, Openssl declared the following weak: RC2, RC4, DES, 3DES, MD5 based
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"des")===FALSE; } );
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"rc2")===FALSE; } );
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"rc4")===FALSE; } );
$ciphers = array_filter( $ciphers, function($c) { return stripos($c,"md5")===FALSE; } );
$ciphers = array_map('strtoupper', $ciphers);
return array_unique($ciphers);
}
/**
* Change OpenSSl Security Level
* @param string $level (low, medium, medium-high, high) or algorithm Method name
*/
public function change_openssl(string $level): bool {
switch (strtolower($level)) {
case 'low':
$this->method = 'AES-128-CBC';
return true;
case 'medium':
$this->method = 'AES-192-CBC';
return true;
case 'high':
$this->method = 'AES-256-CBC';
return true;
} // end of switch
if (in_array(strtoupper($level), $this->list_ssl_methods() )) {
$this->method = strtoupper($level);
return true;
}
return false;
}
/**
* SHA-224, with 224 bit hash values
* SHA-256, with 256 bit hash values
* SHA-384, with 384 bit hash values
* SHA-512, with 512 bit hash values
* SHA-512/224, with 512 bit hash values
* SHA-512/256, with 512 bit hash values
* Among these, SHA-256 and SHA-512 are the most commonly
* accepted and used hash functions computed with 32-bit
* and 64-bit words, respectively. SHA-224 and SHA-384 are
* truncated versions of SHA-256 and SHA-512 respectively,
* computed with different initial values.
*/
public function list_hashes(): array {
$hash = hash_algos();
// Filter out weak Hash FNs
$hash = array_filter( $hash, function($n) { return stripos($n,"crc")===FALSE; } );
$hash = array_filter( $hash, function($n) { return stripos($n,"md")===FALSE; } );
$hash = array_filter( $hash, function($n) { return stripos($n,"sha1")===FALSE; } );
return $hash;
}
public function change_hash(string $hash): bool {
if (in_array(strtolower($hash), $this->list_hashes() )) {
$this->default_hash = strtolower($hash);
return true;
}
return false;
}
public function set_binary_output(bool $bin) {
$this->binary = $bin;
}
public function set_url_encode(bool $url_encode) {
$this->url_encode = $url_encode;
}
/**
* OpenSSL Encrypt text with key
* @param string $text data
* @return string encoded text
*/
public function encrypt(string $text, bool $validate = true): string {
$key = $this->key;
$key = ($validate) ? $this->get_valid_key($key) : $key;
$ivsize = openssl_cipher_iv_length($this->method);
$iv = $this->random_engine->get_bytes($ivsize); // Requires PHP 7
// Encryption key generated by PBKDF2 (since PHP 5.5)
$keys = hash_pbkdf2($this->default_hash, $key, $iv, $this->_iterations, $this->_length, $this->_raw);
$encKey = substr($keys, 0, $this->_key_bytes); // X bit encryption key
$hmacKey = substr($keys, $this->_key_bytes); // X bit hmac key
$ciphertext = openssl_encrypt(
$text,
$this->method,
$encKey,
OPENSSL_RAW_DATA,
$iv
);
\CodeHydrater\bootstrap\common::wipe($text);
\CodeHydrater\bootstrap\common::wipe($key);
\CodeHydrater\bootstrap\common::wipe($encKey);
$hmac = hash_hmac($this->default_hash, $iv . $ciphertext, $hmacKey);
\CodeHydrater\bootstrap\common::wipe($hmacKey);
if (! $this->binary) {
return (! $this->url_encode) ? base64_encode($hmac . $iv . $ciphertext) : \CodeHydrater\misc::base64url_encode($hmac . $iv . $ciphertext);
} else {
return $hmac . $iv . $ciphertext;
}
}
/**
* OpenSSL Decrypt data with key
* @param string $data encoded text
* @return string plain text
*/
public function decrypt(string $data, bool $validate = true): false|string {
$key = $this->key;
$key = ($validate) ? $this->get_valid_key($key) : $key;
if (! $this->binary) {
$text = (! $this->url_encode) ? base64_decode($data) : \CodeHydrater\misc::base64url_decode($data);
} else {
$text = $data;
}
$hmac = substr($text, 0, $this->_length);
$ivsize = openssl_cipher_iv_length($this->method);
$iv = (! $this->binary) ? substr($text, $this->_length, $ivsize) : $ivsize;
$ciphertext = substr($text, $ivsize + $this->_length);
// Generate the encryption and hmac keys
$keys = hash_pbkdf2($this->default_hash, $key, $iv, $this->_iterations, $this->_length, $this->_raw);
$encKey = substr($keys, 0, $this->_key_bytes); // X bit encryption key
$hmacNew = hash_hmac($this->default_hash, $iv . $ciphertext, substr($keys, $this->_key_bytes));
if (! hash_equals($hmac, $hmacNew)) { // to prevent timing attacks/Verify MSG Auth
return false; // Note: hash_equals() requires PHP5.6+
}
$ret = openssl_decrypt(
$ciphertext,
$this->method,
$encKey,
OPENSSL_RAW_DATA,
$iv
);
\CodeHydrater\bootstrap\common::wipe($ciphertext);
\CodeHydrater\bootstrap\common::wipe($key);
\CodeHydrater\bootstrap\common::wipe($encKey);
\CodeHydrater\bootstrap\common::wipe($keys);
return $ret;
}
/**
* Try to make sure you have a valid key
* @param string $key as input
* @return string new key
* @throws Exception
*/
public function get_valid_key(string $key): string {
$ivsize = openssl_cipher_iv_length($this->method);
$key = substr($key, 0, $ivsize * 2);
$keysize = strlen($key);
if ($keysize != $ivsize * 2) {
throw new \Exception('Unable to use ENC key: Bad key size!');
}
if (! ctype_xdigit($key)) {
throw new \Exception('Unable to use ENC key: None HEX Digits!');
}
$ret = bin2hex(pack('H*', $key));
\CodeHydrater\bootstrap\common::wipe($key);
return $ret;
}
/**
* Make a new Secure key
* @return string key
*/
public function generate_valid_key(): string {
$ivsize = openssl_cipher_iv_length($this->method);
return bin2hex($this->random_engine->get_bytes($ivsize));
}
}

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace CodeHydrater\services\sessions;
use \CodeHydrater\enums\compression_method as Method;
/**
* @author Robert Strutts
* @copyright Copyright (c) 2022, Robert Strutts.
* @license MIT
*/
class cookie_sessions_handler_exception extends \Exception {}
class cookie_session_handler implements \SessionHandlerInterface {
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 function __construct($enc, array $options) {
if (isset($options['cookie_domain'])) {
self::$cookie_domain = $options['cookie_domain'];
} else {
self::$cookie_domain = $_SERVER['SERVER_NAME'] ?? '';
}
if (isset($options['cookie_name'])) {
self::$cookie_name = $options['cookie_name'];
}
if (isset($options['cookie_path'])) {
self::$cookie_path = $options['cookie_path'];
}
if (isset($options['cookie_secure'])) {
self::$cookie_secure = $options['cookie_secure'];
} else {
$use_secure = \CodeHydrater\bootstrap\site_helper::get_use_secure();
if ($use_secure === false) {
self::$cookie_secure = false;
}
}
if (isset($options['cookie_HTTP_only'])) {
self::$cookie_HTTP_only = $options['cookie_HTTP_only'];
}
if (isset($options['use_compression'])) {
self::$use_compression = $options['use_compression'];
}
if (isset($options['method'])) {
$method = $options['method'];
} else {
$method = Method::DEFLATE;
}
if (isset($options['level'])) {
$level = $options['level'];
} else {
$level = 4;
}
if (isset($options['enabled'])) {
$enabled = $options['enabled'];
} else {
$enabled = true;
}
$this->compression = new \CodeHydrater\gz_compression($method, $level, $enabled);
$this->enc = $enc;
}
private function write_helper($data): string {
$gc = $this->compression->compress($data);
if ($gc !== false) {
$data = $gc; // data is now compressed
}
if ($this->enc === false) {
return $data; // encryption is off
}
$e = $this->enc->encrypt($data);
return ($e !== false) ? $e : $data; // and encrypted
}
private function read_helper($data): string {
if ($data === null || $data === false) {
return "";
}
if ($this->enc !== false) {
$de = $this->enc->decrypt($data);
if ($de!== false) {
$data = $de; // data is now decrypted
}
}
$gd = $this->compression->decompress($data);
return ($gd !== false) ? $gd : $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;
return $this->read_helper($data) ?: "";
}
public function write($id, $data): bool {
if (headers_sent($file, $line)) {
throw new \CodeHydrater\services\sessions\cookie_sessions_handler_exception("Error headers were already sent by $file on line # $line ");
}
$data = $this->write_helper($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;
}
}

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts <Bob_586@Yahoo.com>
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace CodeHydrater\services\sessions;
class file_session_handler implements SessionHandlerInterface
{
private $savePath;
private $filePrefix;
private $compression;
private $enc;
public function __construct(false|callable $enc, array $config = [])
{
$savePath = $config['save_path'] ?: null;
$filePrefix = $config['prefix'] ?: 'sess_';
$this->filePrefix = $filePrefix;
// Use the system's default save path if none provided
$this->savePath = $savePath ?: $this->getDefaultSavePath();
// Create directory if it doesn't exist
if (!is_dir($this->savePath)) {
mkdir($this->savePath, 0700, true);
}
$method = $config['method'] ?: \CodeHydrater\CompressionMethod::DEFLATE;
$level = $config['level'] ?: 4;
$enabled = $config['enabled'] ?: true;
$this->compression = new \CodeHydrater\gz_compression($method, $level, $enabled);
$this->enc = $enc;
}
private function write_helper($data): string {
$gc = $this->compression->compress($data);
if ($gc !== false) {
$data = $gc; // data is now compressed
}
if ($this->enc === false) {
return $data; // encryption is off
}
$e = $this->enc->encrypt($data);
return ($e !== false) ? $e : $data; // and encrypted
}
private function read_helper($data): string {
if ($data === null || $data === false) {
return "";
}
if ($this->enc !== false) {
$de = $this->enc->decrypt($data);
if ($de!== false) {
$data = $de; // data is now decrypted
}
}
$gd = $this->compression->decompress($data);
return ($gd !== false) ? $gd : $data;
}
/**
* Get default session save path from php.ini
* @return string
*/
private function getDefaultSavePath()
{
return rtrim(ini_get('session.save_path'), '/\\');
}
/**
* Open session
* @param string $savePath
* @param string $sessionName
* @return bool
*/
public function open($savePath, $sessionName): bool
{
// We already set savePath in constructor
return true;
}
/**
* Close session
* @return bool
*/
public function close(): bool
{
return true;
}
/**
* Read session data
* @param string $sessionId
* @return string
*/
public function read($sessionId): string
{
$file = $this->savePath . '/' . $this->filePrefix . $sessionId;
// Lock the file while reading to prevent corruption
if (file_exists($file) && is_readable($file)) {
if ($handle = fopen($file, 'rb')) {
flock($handle, LOCK_SH);
$data = fread($handle, filesize($file));
flock($handle, LOCK_UN);
fclose($handle);
return $this->read_helper($data);
}
}
return '';
}
/**
* Write session data
* @param string $sessionId
* @param string $data
* @return bool
*/
public function write($sessionId, $data): bool
{
$data = $this->write_helper($data);
$file = $this->savePath . '/' . $this->filePrefix . $sessionId;
// Use exclusive lock while writing
if ($handle = fopen($file, 'cb')) {
flock($handle, LOCK_EX);
ftruncate($handle, 0);
fwrite($handle, $data);
fflush($handle);
flock($handle, LOCK_UN);
fclose($handle);
return true;
}
return false;
}
/**
* Destroy session
* @param string $sessionId
* @return bool
*/
public function destroy($sessionId): bool
{
$file = $this->savePath . '/' . $this->filePrefix . $sessionId;
if (file_exists($file)) {
unlink($file);
return true;
}
return false;
}
/**
* Garbage collection
* @param int $maxLifetime
* @return bool
*/
public function gc($maxLifetime): bool
{
foreach (glob($this->savePath . '/' . $this->filePrefix . '*') as $file) {
if (file_exists($file) && filemtime($file) < time() - $maxLifetime) {
unlink($file);
}
}
return true;
}
}

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts <Bob_586@Yahoo.com>
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace CodeHydrater\services\sessions;
class redis_session_handler implements SessionHandlerInterface
{
private $redis;
private $prefix;
private $ttl;
private $lockTimeout = 10;
private $lockRetries = 5;
private $lockWait = 100000; // microseconds
private $enc;
public function __construct(callable $enc, array $config = [])
{
$defaults = [
'host' => '127.0.0.1',
'port' => 6379,
'prefix' => 'PHPREDIS_SESSION:',
'ttl' => null, // null means use session.gc_maxlifetime
'auth' => null,
'persistent' => true,
'database' => 1,
'timeout' => 0,
'read_timeout' => 2,
'retry_interval' => 100
];
$config = array_merge($defaults, $config);
$this->prefix = $config['prefix'];
// Use configured TTL if provided, otherwise use session.gc_maxlifetime
$this->ttl = $config['ttl'] ?? (int)ini_get('session.gc_maxlifetime');
// Fallback to 1440 seconds if neither is set
if ($this->ttl <= 0) {
$this->ttl = 1440;
}
$this->redis = new Redis();
if ($config['persistent']) {
$this->redis->pconnect($config['host'], $config['port'], $config['timeout'], $config['persistent']);
} else {
$this->redis->connect($config['host'], $config['port'], $config['timeout']);
}
if ($config['auth']) {
$this->redis->auth($config['auth']);
}
if ($config['database']) {
$this->redis->select($config['database']);
}
if ($config['read_timeout']) {
$this->redis->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
}
if ($config['retry_interval']) {
$this->redis->setOption(Redis::OPT_RETRY_INTERVAL, $config['retry_interval']);
}
$method = $config['method'] ?: \CodeHydrater\CompressionMethod::DEFLATE;
$level = $config['level'] ?: 4;
$enabled = $config['enabled'] ?: true;
$this->compression = new \CodeHydrater\gz_compression($method, $level, $enabled);
$this->enc = $enc;
}
private function write_helper($data): string {
$gc = $this->compression->compress($data);
if ($gc !== false) {
$data = $gc; // data is now compressed
}
if ($this->enc === false) {
return $data; // encryption is off
}
$e = $this->enc->encrypt($data);
return ($e !== false) ? $e : $data; // and encrypted
}
private function read_helper($data): string {
if ($data === null || $data === false) {
return "";
}
if ($this->enc !== false) {
$de = $this->enc->decrypt($data);
if ($de!== false) {
$data = $de; // data is now decrypted
}
}
$gd = $this->compression->decompress($data);
return ($gd !== false) ? $gd : $data;
}
public function open($savePath, $sessionName): bool
{
return true;
}
public function close(): bool
{
return true;
}
public function read($sessionId): string
{
$key = $this->prefix . $sessionId;
$attempts = 0;
// Simple locking mechanism to prevent race conditions
while ($attempts < $this->lockRetries) {
$lock = $this->redis->setnx($key . '.lock', 1);
if ($lock || $attempts === $this->lockRetries - 1) {
if ($lock) {
$this->redis->expire($key . '.lock', $this->lockTimeout);
}
$data = $this->redis->get($key);
$this->redis->del($key . '.lock');
return $this->read_helper($data) ?: '';
}
usleep($this->lockWait);
$attempts++;
}
return '';
}
public function write($sessionId, $data): bool
{
$data = write_helper($data);
$key = $this->prefix . $sessionId;
// Only write if the session has changed
if (isset($_SESSION['__LAST_ACTIVE__'])) {
$oldData = $this->redis->get($key);
if ($oldData === $data) {
// Just update TTL
return $this->redis->expire($key, $this->ttl);
}
}
$_SESSION['__LAST_ACTIVE__'] = time();
return $this->redis->setex($key, $this->ttl, $data);
}
public function destroy($sessionId): bool
{
$key = $this->prefix . $sessionId;
$this->redis->del($key . '.lock');
return (bool)$this->redis->del($key);
}
public function gc($maxLifetime): bool
{
// Redis handles expiration automatically
return true;
}
}

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright Copyright (c) 2022, Robert Strutts.
* @license MIT
*/
namespace CodeHydrater;
final class session_management {
public static function start(
array $options = [],
string $type = "",
$enc = false
): void {
if (empty($type)) {
$type = bootstrap\configure::get('sessions', 'type');
}
if ($enc === false) {
$exists = bootstrap\registry::get('di')->exists('session_encryption');
if ($exists) {
$enc = bootstrap\registry::get('di')->get_service('session_encryption');
}
}
if ($type === "none" || $type === "php") {
self::make_session_started();
return;
}
$handler = match($type) {
'redis' => new services\sessions\redis_session_handler($enc, $options),
'files' => new services\sessions\file_session_handler($enc, $options),
default => new services\sessions\cookie_session_handler($enc, $options),
};
session_set_save_handler($handler, true);
self::make_session_started();
}
private static function make_session_started(bool $force_secure = false) {
if ((function_exists('session_status') && session_status() !== PHP_SESSION_ACTIVE) || !session_id()) {
$name = bootstrap\configure::get('sessions', 'session_name');
if ($name !== null) {
session_name($name);
}
if (! headers_sent()) {
$use_secure = (bootstrap\site_helper::get_use_secure()) ? 1 : 0;
$use_secure = ($force_secure) ? 1 : $use_secure;
session_start([
'cookie_lifetime' => 0, // until browser is closed
'cookie_secure' => $use_secure, // require secure cookies if HTTPS is used
'use_only_cookies' => 1, // should be 1 to prevent URL attacks
'cookie_httponly' => 1, // should be 1 to disable JavaScript access
'cookie_samesite' => 'Strict', // should be Strict to prevent XSS
// So you need it when you do not want to allow a user to pre-define the session ID value. You normally want to prevent that to reduce the attack surface.
'use_strict_mode' => 1, // Note: Enabling session.use_strict_mode is mandatory for general session security. All sites are advised to enable this.
'use_trans_sid' => 0, // should be kept at the default of 0: URL based session management has additional security risks
]);
}
}
}
public static function has_user_right(string $right): bool {
$rights = (isset($_SESSION['users_rights'])) ? $_SESSION['users_rights'] : false;
if ($rights === false) {
return false;
}
if (! bootstrap\common::is_json($right)) {
return false;
}
$assoc = true; // Use Array format
$a_rights = json_decode($rights, $assoc);
if (in_array($right, $a_rights)) {
return true;
}
return false;
}
public static function get_user_id(): int {
$sid = (isset($_SESSION['user_id'])) ? $_SESSION['user_id'] : 0;
return intval($sid);
}
}

@ -2,13 +2,13 @@
declare(strict_types=1);
namespace tts\traits\database;
namespace CodeHydrater\traits\database;
trait run_sql {
public function run($sql, $bind=""): int {
$pdostmt = $this->pdo->prepare(trim($sql));
if (\bs_tts\common::get_count($bind) > 0) {
if (\CodeHydrater\bootstrap\common::get_count($bind) > 0) {
$exec = $pdostmt->execute($bind);
} else {
$exec = $pdostmt->execute();

@ -8,7 +8,7 @@ declare(strict_types=1);
* @license MIT
*/
namespace tts\traits\database;
namespace CodeHydrater\traits\database;
trait validation {
@ -17,7 +17,7 @@ trait validation {
* @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}`";
$tbl = (\CodeHydrater\bootstrap\common::is_string_found($this->table, "`")) ? $this->table : "`{$this->table}`";
foreach ($this->members as $field => $value) {
if ($field == $this->primary_key) {
continue;
@ -61,7 +61,7 @@ trait validation {
case 'NEWDECIMAL':
$prec = intval($meta['precision']);
$maxlen = $len - $prec;
if (!\bs_tts\common::is_string_found($value, '.')) {
if (!\CodeHydrater\bootstrap\common::is_string_found($value, '.')) {
$this->do_verr("Failed Validation: No Decimal Found in field {$field}");
return false;
}
@ -169,7 +169,7 @@ trait validation {
public function is_valid_mysql_date(string $str): bool {
$date_parts = explode('-', $str);
if (\main_tts\common::count($date_parts) != 3)
if (\CodeHydrater\bootstrap\common::count($date_parts) != 3)
return false;
if ((strlen($date_parts[0]) != 4) || (!is_numeric($date_parts[0])))
return false;
@ -194,9 +194,9 @@ trait validation {
*/
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']);
$exists = \CodeHydrater\bootstrap\registry::get('di')->exists('log');
if ($exists && \CodeHydrater\bootstrap\configure::get('database', 'log_validation_errors') === true) {
$log = \CodeHydrater\bootstrap\registry::get('di')->get_service('log', ['validation_errors']);
$log->write($msg);
}
}

Loading…
Cancel
Save