Robert 4 weeks ago
parent 0606c3bea8
commit e71e8330f9
  1. 28
      src/Framework/Common.php
  2. 147
      src/Framework/Database/BuildSQL.php
  3. 85
      src/Framework/Database/Grammar/Grammar.php
  4. 54
      src/Framework/Database/Grammar/MySQLGrammar.php
  5. 58
      src/Framework/Database/Grammar/SQLiteGrammar.php
  6. 204
      src/Framework/Database/Model.php
  7. 8
      src/Framework/Database/Paginate.php
  8. 2
      src/Framework/ErrorHandler.php
  9. 7
      src/Framework/SaferOutput.php
  10. 16
      src/Framework/Services/Sessions/FileSessionHandler.php
  11. 42
      src/Framework/Services/Sessions/RedisSessionHandler.php
  12. 1
      src/Framework/SessionManagement.php
  13. 745
      src/Framework/Trait/Database/QueryBuilder.php
  14. 1
      src/Framework/Trait/Database/Validation.php
  15. 11
      src/Framework/Trait/Security/CsrfTokenFunctions.php

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace IOcornerstone\Framework; namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{ use IOcornerstone\Framework\{
Configure,
Console, Console,
Logger, Logger,
String\StringFacade as F, String\StringFacade as F,
@ -203,6 +204,31 @@ final class Common
} }
} }
private static function allowDump(): bool
{
$hasShowDumps = Configure::has('security', 'show_dumps');
if (! $hasShowDumps) {
return false;
}
$b_show_dumps = Configure::get('security', 'show_dumps');
if ($b_show_dumps === false || $b_show_dumps === null) {
return false;
}
$hasShowDumps = Configure::has('debugger', 'allow_show_dumps');
if (! $hasShowDumps) {
return false;
}
$b_allow_show_dumps = Configure::get('debugger', 'allow_show_dumps');
if ($b_allow_show_dumps === false || $b_allow_show_dumps === null) {
return false;
}
return true;
}
/** /**
* Variable Dump and exit * Variable Dump and exit
* Configure of security for show_dumps must be true for debugging. * Configure of security for show_dumps must be true for debugging.
@ -214,7 +240,7 @@ final class Common
endDump $end = endDump::EXIT_AND_STOP endDump $end = endDump::EXIT_AND_STOP
): void ): void
{ {
if (\IOcornerstone\Framework\Configure::get('security', 'show_dumps') !== true) { if (! self::allowDump()) {
return; return;
} }
$isConsole = Console::isConsole(); $isConsole = Console::isConsole();

@ -85,4 +85,151 @@ class BuildSQL
. "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"; . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
} }
public function generateSelectAll(\PDO $pdo, string $table): string
{
$sql = "
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$table]);
$columns = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$line = '`' . $row['COLUMN_NAME'] . '`';
$columns[] = $line;
}
$out = "\$query = \"SELECT " . implode(", ", $columns) . " FROM `{$table}`\";" . PHP_EOL;
$out .= "\${$table}Stmt = \$this->pdo->query(\$query);
while (\${$table}Row = \${$table}Stmt->fetch(\\PDO::FETCH_ASSOC)) {
\$a[] = \"\";";
foreach($columns as $col) {
$sCol = trim($col, "`");
$out .= "\$a['{$sCol}'] = \${$table}Row['{$sCol}'] ?? \"\";" . PHP_EOL;
}
return $out;
}
public function generateSelect(\PDO $pdo, string $table): string
{
$sql = "
SELECT COLUMN_NAME, COLUMN_KEY
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$table]);
$columns = [];
$primary = [];
$where = "";
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$line = '`' . $row['COLUMN_NAME'] . '`';
$columns[] = $line;
if ($row['COLUMN_KEY'] === 'PRI') {
$primary[] = '`' . $row['COLUMN_NAME'] . '`';
}
}
if (!empty($primary)) {
$where = " WHERE {$primary} = \$id LIMIT 1";
}
return "SELECT " . implode(", ", $columns) . " FROM `{$table}` $where";
}
public function generateInserts(\PDO $pdo, string $table): string
{
$sql = "
SELECT
COLUMN_NAME,
IS_NULLABLE,
COLUMN_DEFAULT,
COLUMN_KEY
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$table]);
$columns = [];
$cleanColumns = [];
$extras = [];
$primary = [];
$pKey = "";
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$cleanColumns[] = $row['COLUMN_NAME'];
$line = '`' . $row['COLUMN_NAME'] . '`';
if ($row['IS_NULLABLE'] === 'NO') {
$extra[$row['COLUMN_NAME']] = ' NOT NULL';
}
if ($row['COLUMN_DEFAULT'] !== null) {
$default = $row['COLUMN_DEFAULT'];
if (strtoupper($default) === 'CURRENT_TIMESTAMP') {
$extra[$row['COLUMN_NAME']] = ' DEFAULT CURRENT_TIMESTAMP';
} else {
$extra[$row['COLUMN_NAME']] = ' DEFAULT ' . $pdo->quote($default);
}
}
$columns[] = $line;
if ($row['COLUMN_KEY'] === 'PRI') {
$primary[] = '`' . $row['COLUMN_NAME'] . '`';
}
}
if (!empty($primary)) {
$pKey = '// PRIMARY KEY (' . implode(', ', $primary) . ')';
}
$data = "";
foreach($extra as $key => $value) {
$data .= '//' . $key . ' ' . $value . PHP_EOL;
}
return $data . "INSERT INTO `{$table}` (\n "
. implode(",\n ", $columns)
. "\n) VALUES (\n"
. implode(",\n :", $cleanColumns)
. "\n)\n $pKey" . $this->bindInsert($table, $columns);
}
private function bindInsert(string $table, array $columns): string
{
$out = "\n private function {$table}Inserts(\$p, array \$a): bool\n\t{\t\t try {\n";
foreach($columns as $col) {
$sCol = trim($col, "`");
$out .= "\$p->bindParam(':{$sCol}', \$a['{$sCol}'])". PHP_EOL;
}
$out .= 'return $p->execute();
} catch (\PDOException $e) {
echo $e->getMessage();
return false;
}
}';
return $out;
}
} }

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Database\Grammar;
/**
* Description of Grammar
*
* @author Robert Strutts
*/
abstract class Grammar
{
/**
* Compile an entire SELECT statement.
*/
public function compileSelect($query): string
{
$sql = 'SELECT ';
$sql .= $query->select;
$sql .= ' FROM ';
$sql .= $this->wrap($query->table);
$sql .= $this->compileJoins($query);
$sql .= $this->compileWhere($query);
$sql .= $this->compileGroupBy($query);
$sql .= $this->compileHaving($query);
$sql .= $this->compileOrderBy($query);
$sql .= $this->compileLimit($query);
$sql .= $this->compileOffset($query);
return $sql;
}
public function compileLimit(?int $limit): string
{
return $limit !== null ? " LIMIT {$limit}" : '';
}
public function compileOffset(?int $offset): string
{
return $offset !== null ? " OFFSET {$offset}" : '';
}
protected function compileOrderBy($query): string
{
if (!$query->orders)
return '';
$parts = [];
foreach ($query->orders as $order) {
$parts[] =
$this->wrap($order['column'])
.' '
.$order['direction'];
}
return ' ORDER BY '.implode(',', $parts);
}
abstract public function wrap(string $identifier): string;
abstract public function compileDate(string $column): string;
abstract public function compileTime(string $column): string;
abstract public function compileYear(string $column): string;
abstract public function compileMonth(string $column): string;
abstract public function compileDay(string $column): string;
abstract public function compileJsonContains(string $column): string;
}

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Database\Grammar;
/**
* Description of MySQLGrammar
*
* @author Robert Strutts
*/
class MySQLGrammar extends Grammar
{
public function wrap(string $column): string
{
return "`{$column}`";
}
public function compileYear(string $column): string
{
return "YEAR({$this->wrap($column)})";
}
public function compileMonth(string $column): string
{
return "MONTH({$this->wrap($column)})";
}
public function compileDay(string $column): string
{
return "DAY({$this->wrap($column)})";
}
public function compileDate(string $column): string
{
return "DATE({$this->wrap($column)})";
}
public function compileTime(string $column): string
{
return "TIME({$this->wrap($column)})";
}
public function compileJsonContains(string $column): string
{
return "JSON_CONTAINS({$this->wrap($column)}, ?)";
}
}

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Database\Grammar;
/**
* Description of SQLiteGrammar
*
* @author Robert Strutts
*/
class SQLiteGrammar extends Grammar
{
public function wrap(string $column): string
{
return "\"{$column}\"";
}
public function compileYear(string $column): string
{
return "strftime('%Y',".$this->wrap($column).")";
}
public function compileMonth(string $column): string
{
return "strftime('%m',".$this->wrap($column).")";
}
public function compileDay(string $column): string
{
return "strftime('%d',".$this->wrap($column).")";
}
public function compileDate(string $column): string
{
return "date(".$this->wrap($column).")";
}
public function compileTime(string $column): string
{
return "time(".$this->wrap($column).")";
}
public function compileJsonContains(string $column): string
{
return "EXISTS (
SELECT 1
FROM json_each(".$this->wrap($column).")
WHERE value=?
)";
}
}

@ -22,11 +22,11 @@ class Model {
use RunSql; use RunSql;
use Validation; use Validation;
const SUCCESSFUL_SAVE = 1; public const int SUCCESSFUL_SAVE = 1;
const DUPLICATE_FOUND = 2; public const int DUPLICATE_FOUND = 2;
const VALIDATION_ERROR = 3; public const int VALIDATION_ERROR = 3;
const PRE_SAVE_FAILED = 4; public const int PRE_SAVE_FAILED = 4;
const POST_SAVE_FAILED = 5; public const int POST_SAVE_FAILED = 5;
private $members = []; private $members = [];
private $validationMembers = []; private $validationMembers = [];
@ -72,6 +72,23 @@ class Model {
return false; return false;
} }
public function beginTransaction()
{
$this->pdo->beginTransaction();
}
public function commit()
{
$this->pdo->commit();
}
public function rollBack()
{
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
}
private function doDump(array $data) { private function doDump(array $data) {
echo "<pre style=\"border: 1px solid #000; overflow: auto; margin: 0.5em;\">"; echo "<pre style=\"border: 1px solid #000; overflow: auto; margin: 0.5em;\">";
print_r($data); print_r($data);
@ -96,7 +113,8 @@ class Model {
continue; continue;
} }
if (in_array($key, $onlyThese) || !count($onlyThese)) { if (in_array($key, $onlyThese) || !count($onlyThese)) {
$this->members[$key] = (! empty($data)) ? $data : ""; $saferKey = $this->sanitizeFieldName($key);
$this->members[$saferKey] = (! empty($data)) ? $data : "";
} }
} }
} }
@ -114,7 +132,8 @@ class Model {
} }
public function setMember(string $member, string $data): void { public function setMember(string $member, string $data): void {
$this->members[$member] = $data; $saferMember = $this->sanitizeFieldName($member);
$this->members[$saferMember] = $data;
} }
public function hasMember($member): bool { public function hasMember($member): bool {
@ -151,6 +170,30 @@ class Model {
unset($this->validationMembers[$key]); unset($this->validationMembers[$key]);
} }
public function createFromArray(array $data): object
{
$model = new \stdClass();
foreach($data as $key => $value) {
$model->$key = $value;
}
return $model;
}
public function getColumns(array $data): string
{
return implode(', ', array_keys($data));
}
public function getPlaceHolders(array $data): string
{
return implode(', ', array_fill(0, count($data), '?'));
}
public function getValues(array $data): array
{
return array_values($data);
}
private function cleanup($bind) { private function cleanup($bind) {
if (!is_array($bind)) { if (!is_array($bind)) {
if (!empty($bind)) { if (!empty($bind)) {
@ -184,12 +227,48 @@ class Model {
} }
public function load(int $id): bool{ public function sanitizeFieldName(string $field): string {
// Remove dangerous characters
$field = preg_replace('/[^a-zA-Z0-9_.]/', '', $field);
// Remove SQL keywords
$field = preg_replace('/\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|UNION|WHERE|FROM)\b/i', '', $field);
return $field;
}
private function saferFields(string $fields): string
{
$safeFields = array_map([$this, 'sanitizeFieldName'], explode(',', $fields));
$safeFields = array_filter($safeFields); // Remove empty entries
if (empty($safeFields)) {
throw new Exception('Invalid field names');
}
return implode(', ', $safeFields);
}
private function getSafeSelect(array|string $fields): string
{
if ($fields === "*") {
return "*";
}
if (is_array($fields) && Common::isArrayWithData($fields)) {
if (Common::hasKeys($fields)) {
$fields = implode(', ', $this->filter($this->table, $fields));
} else {
$fields = implode(', ', $fields);
}
}
return $this->saferFields($fields);
}
public function load(int $id, array|string $fields = "*"): bool{
if (method_exists($this, 'pre_load')) { if (method_exists($this, 'pre_load')) {
$this->pre_load(); $this->pre_load();
} }
$sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1"; $sql = "SELECT {$this->getSafeSelect($fields)} FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1";
$query = $this->pdo->prepare($sql); $query = $this->pdo->prepare($sql);
$query->execute(array($id)); $query->execute(array($id));
if ($query === false) { if ($query === false) {
@ -207,55 +286,6 @@ class Model {
return true; return true;
} }
public function insert($table, $info) {
$fields = $this->filter($table, $info);
if (strrpos($table, "`") === false) {
$table = "`{$table}`";
}
$sql = "INSERT INTO {$table} (" . implode(", ", $fields) . ") VALUES (:" . implode(", :", $fields) . ");";
$bind = array();
foreach ($fields as $field) {
$bind[":$field"] = $info[$field];
}
return $this->run($sql, $bind);
}
public function update($table, $info, $where, $bind = "") {
$bind = $this->cleanup($bind);
$fields = $this->filter($table, $info);
if (strrpos($table, "`") === false) {
$table = "`{$table}`";
}
$sql = "UPDATE {$table} SET ";
$f = 0;
foreach ($fields as $key => $value) {
if ($f > 0) {
$sql .= ", ";
}
$f++;
$value = trim($value);
if (strrpos($value, "`") === false) {
$cf = '`' . $value . '`';
} else {
$cf = $value;
}
$sql .= $cf . " = :update_" . $value;
$bind[":update_$value"] = $info[$value];
}
$sql .= " WHERE " . $where . ";";
return $this->run($sql, $bind);
}
public static function makeDbTimeStamp(): string { public static function makeDbTimeStamp(): string {
return TimeZones::convertTimeZone(['format' => 'database', 'timezone' => 'UTC']); return TimeZones::convertTimeZone(['format' => 'database', 'timezone' => 'UTC']);
} }
@ -281,8 +311,8 @@ class Model {
} }
} }
if ($this->hasMember('modified')) { if ($this->hasMember('updated_at')) {
$this->setMember('modified', self::makeDbTimeStamp()); $this->setMember('updated_at', self::makeDbTimeStamp());
} }
if (empty($this->members[$this->primaryKey])) { if (empty($this->members[$this->primaryKey])) {
@ -293,8 +323,8 @@ class Model {
} }
} }
if ($this->hasMember('created')) { if ($this->hasMember('created_at')) {
$this->setMember('created', self::makeDbTimeStamp()); $this->setMember('created_at', self::makeDbTimeStamp());
} }
$this->members[$this->primaryKey] = $this->insert($this->table, $this->members); $this->members[$this->primaryKey] = $this->insert($this->table, $this->members);
@ -315,6 +345,56 @@ class Model {
return self::SUCCESSFUL_SAVE; return self::SUCCESSFUL_SAVE;
} }
private function insert($table, $info) {
$fields = $this->filter($table, $info);
if (strrpos($table, "`") === false) {
$table = "`{$table}`";
}
$sql = "INSERT INTO {$table} (" . implode(", ", $fields) . ") VALUES (:" . implode(", :", $fields) . ");";
$bind = array();
foreach ($fields as $field) {
$bind[":$field"] = $info[$field];
}
return $this->run($sql, $bind);
}
private function update($table, $info, $where, $bind = "") {
$bind = $this->cleanup($bind);
$fields = $this->filter($table, $info);
if (strrpos($table, "`") === false) {
$table = "`{$table}`";
}
$sql = "UPDATE {$table} SET ";
$f = 0;
foreach ($fields as $key => $value) {
if ($f > 0) {
$sql .= ", ";
}
$f++;
$value = trim($value);
if (strrpos($value, "`") === false) {
$cf = '`' . $value . '`';
} else {
$cf = $value;
}
$sql .= $cf . " = :update_" . $value;
$bind[":update_$value"] = $info[$value];
}
$sql .= " WHERE " . $where . ";";
return $this->run($sql, $bind);
}
/** /**
* This FN requires SSL connection to the database. * This FN requires SSL connection to the database.
* So, the KEY does not get exposed!!!! * So, the KEY does not get exposed!!!!

@ -117,7 +117,13 @@ class Paginate
private function do_href(int $limit, int $page): string private function do_href(int $limit, int $page): string
{ {
return 'href="' . $this->_url . $this->_url_limit . $limit . $this->_url_page . $page . '"'; $queryParmas = $_GET;
unset($queryParmas['limit']);
unset($queryParmas['page']);
$q = http_build_query($queryParmas);
$querySeperator = (str_contains($this->_url_limit, "?")) ? "&" : "?";
$querySeperator = (empty($q)) ? "" : $querySeperator;
return 'href="' . $this->_url . $this->_url_limit . $limit . $this->_url_page . $page . $querySeperator . $q . '"';
} }
public function createLinks(int $links = 7, string $list_class = "ui pagination menu", string $item = "item"): string public function createLinks(int $links = 7, string $list_class = "ui pagination menu", string $item = "item"): string

@ -310,7 +310,7 @@ final class ErrorHandler
$out .= "On Line # " . $e->getLine() . PHP_EOL; $out .= "On Line # " . $e->getLine() . PHP_EOL;
$out .= $e->getTraceAsString() . PHP_EOL; $out .= $e->getTraceAsString() . PHP_EOL;
echo $color . $out . PHP_EOL; fwrite(STDERR, $color . $out . PHP_EOL);
} }
private function getCleanHTML(string $in): string private function getCleanHTML(string $in): string

@ -118,12 +118,17 @@ class SaferOutput
return ($object instanceof \JsonException); return ($object instanceof \JsonException);
} }
// Escape URL output // Escape URL output by Encoding it
public static function u(string $string): string public static function u(string $string): string
{ {
return urlencode($string); return urlencode($string);
} }
public static function urlDecode(string $string): string
{
return urldecode($string);
}
/* /*
* Encode HTML kindof... The problem with htmlentities() is that it is not * Encode HTML kindof... The problem with htmlentities() is that it is not
* very powerful, in fact, it does not escape single quotes, cannot detect * very powerful, in fact, it does not escape single quotes, cannot detect

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @author Robert Strutts <Bob_586@Yahoo.com> * @author Robert Strutts
* @copyright (c) 2025, Robert Strutts * @copyright (c) 2025, Robert Strutts
* @license MIT * @license MIT
*/ */
@ -13,18 +13,18 @@ namespace IOcornerstone\Framework\Services\Sessions;
use IOcornerstone\Framework\GzCompression; use IOcornerstone\Framework\GzCompression;
use IOcornerstone\Framework\Enum\CompressionMethod as Method; use IOcornerstone\Framework\Enum\CompressionMethod as Method;
class FileSessionHandler implements SessionHandlerInterface class FileSessionHandler implements \SessionHandlerInterface
{ {
private $savePath; private $savePath;
private $filePrefix; private $filePrefix;
private $compression; private $compression;
private $enc; private $enc;
public function __construct(false|callable $enc, array $config = []) public function __construct($enc, array $config = [])
{ {
$savePath = $config['save_path'] ?: null; $savePath = $config['save_path'] ?? null;
$filePrefix = $config['prefix'] ?: 'sess_'; $filePrefix = $config['prefix'] ?? 'sess_';
$this->filePrefix = $filePrefix; $this->filePrefix = $filePrefix;
@ -36,9 +36,9 @@ class FileSessionHandler implements SessionHandlerInterface
mkdir($this->savePath, 0700, true); mkdir($this->savePath, 0700, true);
} }
$method = $config['method'] ?: Method::DEFLATE; $method = $config['method'] ?? Method::DEFLATE;
$level = $config['level'] ?: 4; $level = $config['level'] ?? 4;
$enabled = $config['enabled'] ?: true; $enabled = $config['enabled'] ?? true;
$this->compression = new GzCompression($method, $level, $enabled); $this->compression = new GzCompression($method, $level, $enabled);

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @author Robert Strutts <Bob_586@Yahoo.com> * @author Robert Strutts
* @copyright (c) 2025, Robert Strutts * @copyright (c) 2025, Robert Strutts
* @license MIT * @license MIT
*/ */
@ -13,7 +13,7 @@ namespace IOcornerstone\Framework\Services\Sessions;
use IOcornerstone\Framework\GzCompression; use IOcornerstone\Framework\GzCompression;
use IOcornerstone\Framework\Enum\CompressionMethod as Method; use IOcornerstone\Framework\Enum\CompressionMethod as Method;
class RedisSessionHandler implements SessionHandlerInterface class RedisSessionHandler implements \SessionHandlerInterface
{ {
private $redis; private $redis;
private $prefix; private $prefix;
@ -23,7 +23,7 @@ class RedisSessionHandler implements SessionHandlerInterface
private $lockWait = 100000; // microseconds private $lockWait = 100000; // microseconds
private $enc; private $enc;
public function __construct(callable $enc, array $config = []) public function __construct($enc, array $config = [])
{ {
$defaults = [ $defaults = [
'host' => '127.0.0.1', 'host' => '127.0.0.1',
@ -50,12 +50,26 @@ class RedisSessionHandler implements SessionHandlerInterface
$this->ttl = 1440; $this->ttl = 1440;
} }
$this->redis = new Redis(); $this->redis = new \Redis();
if ($config['persistent']) { if ($config['persistent']) {
$this->redis->pconnect($config['host'], $config['port'], $config['timeout'], $config['persistent']); $this->redis->pconnect(
host: $config['host'],
port: $config['port'],
timeout: $config['timeout'],
persistent_id: null,
retry_interval: $config['retry_interval'],
read_timeout: $config['read_timeout']
);
} else { } else {
$this->redis->connect($config['host'], $config['port'], $config['timeout']); $this->redis->connect(
host: $config['host'],
port: $config['port'],
timeout: $config['timeout'],
persistent_id: null,
retry_interval: $config['retry_interval'],
read_timeout: $config['read_timeout']
);
} }
if ($config['auth']) { if ($config['auth']) {
@ -67,16 +81,12 @@ class RedisSessionHandler implements SessionHandlerInterface
} }
if ($config['read_timeout']) { if ($config['read_timeout']) {
$this->redis->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']); $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
} }
if ($config['retry_interval']) { $method = $config['method'] ?? Method::DEFLATE;
$this->redis->setOption(Redis::OPT_RETRY_INTERVAL, $config['retry_interval']); $level = $config['level'] ?? 4;
} $enabled = $config['enabled'] ?? true;
$method = $config['method'] ?: Method::DEFLATE;
$level = $config['level'] ?: 4;
$enabled = $config['enabled'] ?: true;
$this->compression = new GzCompression($method, $level, $enabled); $this->compression = new GzCompression($method, $level, $enabled);
@ -135,7 +145,7 @@ class RedisSessionHandler implements SessionHandlerInterface
$data = $this->redis->get($key); $data = $this->redis->get($key);
$this->redis->del($key . '.lock'); $this->redis->del($key . '.lock');
return $this->readHelper($data) ?: ''; return $this->readHelper($data) ?? '';
} }
usleep($this->lockWait); usleep($this->lockWait);
@ -147,7 +157,7 @@ class RedisSessionHandler implements SessionHandlerInterface
public function write($sessionId, $data): bool public function write($sessionId, $data): bool
{ {
$data = write_helper($data); $data = $this->writeHelper($data);
$key = $this->prefix . $sessionId; $key = $this->prefix . $sessionId;
// Only write if the session has changed // Only write if the session has changed

@ -15,7 +15,6 @@ use IOcornerstone\Framework\Services\Sessions\{
CookieSessionHandler as CookieSes CookieSessionHandler as CookieSes
}; };
use IOcornerstone\Framework\{ use IOcornerstone\Framework\{
Common,
Configure, Configure,
Registry Registry
}; };

@ -0,0 +1,745 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Trait\Database;
use IOcornerstone\Framework\Database\Grammar\{
MySQLGrammar,
SQLiteGrammar,
};
/**
*
* @author Robert Strutts
*/
trait QueryBuilder
{
protected $grammar;
protected array $wheres = [];
protected array $bindings = [];
protected array $joins = [];
protected array $groups = [];
protected array $havings = [];
protected array $orders = [];
protected ?string $orderBy = null;
protected ?int $limit = null;
protected ?int $offset = null;
protected string $select = '*';
public function select(array $columns): static
{
$this->select = implode(',', $columns);
return $this;
}
public function where(
string $column,
string $operator,
mixed $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Basic',
'column' => $column,
'operator' => $operator,
'value' => $value
];
$this->bindings[] = $value;
return $this;
}
public function orWhere(
string $column,
string $operator,
mixed $value
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'Basic',
'column' => $column,
'operator' => $operator,
'value' => $value
];
$this->bindings[] = $value;
return $this;
}
public function whereNull(string $column): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Null',
'column' => $column
];
return $this;
}
public function orWhereNull(string $column): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'Null',
'column' => $column
];
return $this;
}
public function whereNotNull(string $column): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'NotNull',
'column' => $column
];
return $this;
}
public function orWhereNotNull(string $column): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'NotNull',
'column' => $column
];
return $this;
}
public function whereIn(
string $column,
array $values
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'In',
'column' => $column,
'count' => count($values)
];
foreach ($values as $value)
$this->bindings[] = $value;
return $this;
}
public function orWhereIn(
string $column,
array $values
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'In',
'column' => $column,
'count' => count($values)
];
foreach ($values as $value)
$this->bindings[] = $value;
return $this;
}
public function whereNotIn(
string $column,
array $values
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'NotIn',
'column' => $column,
'count' => count($values)
];
foreach ($values as $value)
$this->bindings[] = $value;
return $this;
}
public function whereBetween(
string $column,
mixed $from,
mixed $to
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Between',
'column' => $column
];
$this->bindings[] = $from;
$this->bindings[] = $to;
return $this;
}
public function whereNotBetween(
string $column,
mixed $from,
mixed $to
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'NotBetween',
'column' => $column
];
$this->bindings[] = $from;
$this->bindings[] = $to;
return $this;
}
public function whereLike(
string $column,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Like',
'column' => $column
];
$this->bindings[] = $value;
return $this;
}
public function whereNotLike(
string $column,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'NotLike',
'column' => $column
];
$this->bindings[] = $value;
return $this;
}
public function whereColumn(
string $left,
string $operator,
string $right
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Column',
'left' => $left,
'operator' => $operator,
'right' => $right
];
return $this;
}
public function whereRaw(
string $sql,
array $bindings = []
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Raw',
'sql' => $sql
];
foreach ($bindings as $binding) {
$this->bindings[] = $binding;
}
return $this;
}
public function whereDate(
string $column,
string $operator,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Date',
'column' => $column,
'operator' => $operator,
];
$this->bindings[] = $value;
return $this;
}
public function whereTime(
string $column,
string $operator,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Time',
'column' => $column,
'operator' => $operator
];
$this->bindings[] = $value;
return $this;
}
public function whereYear(
string $column,
string $operator,
int $year
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Year',
'column' => $column,
'operator' => $operator
];
$this->bindings[] = $year;
return $this;
}
public function whereMonth(
string $column,
string $operator,
int $month
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Month',
'column' => $column,
'operator' => $operator
];
$this->bindings[] = $month;
return $this;
}
public function whereDay(
string $column,
string $operator,
int $day
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'Day',
'column' => $column,
'operator' => $operator
];
$this->bindings[] = $day;
return $this;
}
public function orWhereBetween(
string $column,
mixed $from,
mixed $to
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'Between',
'column' => $column
];
$this->bindings[] = $from;
$this->bindings[] = $to;
return $this;
}
public function orWhereNotBetween(
string $column,
mixed $from,
mixed $to
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'NotBetween',
'column' => $column
];
$this->bindings[] = $from;
$this->bindings[] = $to;
return $this;
}
public function orWhereLike(
string $column,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'Like',
'column' => $column
];
$this->bindings[] = $value;
return $this;
}
public function orWhereNotLike(
string $column,
string $value
): static
{
$this->wheres[] = [
'boolean' => 'OR',
'type' => 'NotLike',
'column' => $column
];
$this->bindings[] = $value;
return $this;
}
public function whereJsonContains(
string $column,
mixed $value
): static
{
$this->wheres[] = [
'boolean' => 'AND',
'type' => 'JsonContains',
'column' => $column
];
$this->bindings[] = json_encode($value);
return $this;
}
public function orderBy(
string $column,
string $direction = 'ASC'
): static
{
$direction = strtoupper($direction);
$this->orderBy = "{$column} {$direction}";
return $this;
}
public function limit(int $limit): static
{
$this->limit = $limit;
return $this;
}
public function leftJoin(
string $table,
string $left,
string $operator,
string $right
): static
{
$this->joins[] = "LEFT JOIN {$table}
ON {$left} {$operator} {$right}";
return $this;
}
public function join(
string $table,
string $left,
string $operator,
string $right
): static
{
$this->joins[] = "INNER JOIN {$table} ON {$left} {$operator} {$right}";
return $this;
}
private function buildWhere(string $sql): string
{
foreach ($this->wheres as $index => $where) {
if ($index > 0) {
$sql .= ' ' . $where['boolean'] . ' ';
}
switch ($where['type']) {
case 'Null':
$sql .= "{$where['column']} IS NULL";
break;
case 'NotNull':
$sql .= "{$where['column']} IS NOT NULL";
break;
case 'Like':
$sql .= "{$where['column']} LIKE ?";
break;
case 'NotLike':
$sql .= "{$where['column']} NOT LIKE ?";
break;
case 'Between':
$sql .= "{$where['column']} BETWEEN ? AND ?";
break;
case 'NotBetween':
$sql .= "{$where['column']} NOT BETWEEN ? AND ?";
break;
case 'In':
$sql .= "{$where['column']} IN (" .
implode(',', array_fill(0, $where['count'], '?')) . ")";
break;
case 'NotIn':
$sql .= "{$where['column']} NOT IN (" .
implode(',', array_fill(0, $where['count'], '?')) . ")";
break;
case 'Column':
$sql .= "{$where['left']} {$where['operator']} {$where['right']}";
break;
case 'Raw':
$sql .= $where['sql'];
break;
case 'Basic':
$sql .= $this->grammar->wrap($where['column']);
$sql .= " {$where['operator']} ?";
break;
case 'Year':
$sql .= $this->grammar->compileYear(
$where['column']
);
$sql .= " {$where['operator']} ?";
break;
case 'Month':
$sql .= $this->grammar->compileMonth(
$where['column']
);
$sql .= " {$where['operator']} ?";
break;
case 'Day':
$sql .= $this->grammar->compileDay(
$where['column']
);
$sql .= " {$where['operator']} ?";
break;
case 'Date':
$sql .= $this->grammar->compileDate(
$where['column']
);
$sql .= " {$where['operator']} ?";
break;
case 'Time':
$sql .= $this->grammar->compileTime(
$where['column']
);
$sql .= " {$where['operator']} ?";
break;
case 'JsonContains':
$sql .= $this->grammar->compileJsonContains(
$where['column']
);
break;
}
}
return $sql;
}
protected function buildSql(): string
{
if ($this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'mysql') {
$this->grammar = new MySQLGrammar();
} else {
$this->grammar = new SQLiteGrammar();
}
$sql = "SELECT {$this->select} FROM {$this->table}";
if ($this->joins) {
$sql .= ' ' . implode(' ', $this->joins);
}
if ($this->wheres) {
$sql = $this->buildWhere($sql . " WHERE ");
}
if ($this->orderBy) {
$sql .= ' ORDER BY ' . $this->orderBy;
}
if ($this->limit) {
$sql .= ' LIMIT ' . $this->limit;
}
return $sql;
}
public function groupBy(string ...$columns): static
{
foreach ($columns as $column) {
$this->groups[] = $column;
}
return $this;
}
public function having(
string $column,
string $operator,
mixed $value
): static
{
$this->havings[] = "{$column} {$operator} ?";
$this->bindings[] = $value;
return $this;
}
public function offset(int $offset): static
{
$this->offset = $offset;
return $this;
}
private function getRow(): array
{
$stmt = $this->pdo->prepare(
$this->buildSql()
);
$stmt->execute($this->bindings);
return $stmt->fetch(\PDO::FETCH_ASSOC);
}
public function get()
{
$stmt = $this->pdo->prepare(
$this->buildSql()
);
$stmt->execute($this->bindings);
return $stmt;
}
public function first(): ?array
{
$this->limit(1);
$rows = $this->getRow();
return $rows ?? null;
}
public function count(): int
{
$this->select = 'COUNT(' . $this->primaryKey . ')';
$stmt = $this->pdo->prepare(
$this->buildSql()
);
$stmt->execute($this->bindings);
return (int) $stmt->fetchColumn();
}
public function sum(string $column): int
{
$this->select = "SUM({$column})";
$stmt = $this->pdo->prepare(
$this->buildSql()
);
$stmt->execute($this->bindings);
return (int) $stmt->fetchColumn();
}
public function exists(): bool
{
$this->limit(1);
return $this->count() > 0;
}
public function toSql(): string
{
return $this->buildSql();
}
}

@ -12,7 +12,6 @@ namespace IOCornerstone\Framework\Trait\Database;
use IOcornerstone\Framework\{ use IOcornerstone\Framework\{
Configure, Configure,
Common, Common,
Registry,
Logger, Logger,
}; };

@ -18,7 +18,8 @@ use IOcornerstone\Framework\{
trait CsrfTokenFunctions trait CsrfTokenFunctions
{ {
const CSRF_TOKEN_POOL_SIZE_LIMIT = 15; // Total Sessions to Keep alive private const int CSRF_TOKEN_POOL_SIZE_LIMIT = 15; // Total Sessions to Keep alive
public const string CSRF_TOKEN_FIELD_NAME = "csrf_token";
/** /**
* Set Session to use CSRF Token * Set Session to use CSRF Token
* Useful for JSON data... * Useful for JSON data...
@ -41,13 +42,13 @@ trait CsrfTokenFunctions
public static function csrfTokenTag(): string public static function csrfTokenTag(): string
{ {
$token = self::createCsrfToken(); $token = self::createCsrfToken();
return "<input type=\"hidden\" id=\"csrf_token\" name=\"csrf_token\" value=\"" . $token . "\">"; return "<input type=\"hidden\" id=\"" . self::CSRF_TOKEN_FIELD_NAME . "\" name=\"" . self::CSRF_TOKEN_FIELD_NAME . "\" value=\"" . $token . "\">";
} }
public static function csrfTokenStillValid(string $csrfTokenKeyName = ""): bool public static function csrfTokenStillValid(string $csrfTokenKeyName = ""): bool
{ {
if (empty($csrfTokenKeyName)) { if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? ""; $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
} }
$validTimeStamp = self::csrfTokenIsValid($csrfTokenKeyName); $validTimeStamp = self::csrfTokenIsValid($csrfTokenKeyName);
@ -109,7 +110,7 @@ trait CsrfTokenFunctions
private static function csrfTokenIsValid(string $csrfTokenKeyName = ""): false|int private static function csrfTokenIsValid(string $csrfTokenKeyName = ""): false|int
{ {
if (empty($csrfTokenKeyName)) { if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? ""; $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
} }
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) { if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
@ -142,7 +143,7 @@ trait CsrfTokenFunctions
private static function destroyCsrfToken(string $csrfTokenKeyName = ""): bool private static function destroyCsrfToken(string $csrfTokenKeyName = ""): bool
{ {
if (empty($csrfTokenKeyName)) { if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? ""; $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
} }
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) { if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
return false; return false;

Loading…
Cancel
Save