diff --git a/src/Framework/Common.php b/src/Framework/Common.php index b2f2aa6..833db00 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace IOcornerstone\Framework; use IOcornerstone\Framework\{ + Configure, Console, Logger, 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 * Configure of security for show_dumps must be true for debugging. @@ -214,7 +240,7 @@ final class Common endDump $end = endDump::EXIT_AND_STOP ): void { - if (\IOcornerstone\Framework\Configure::get('security', 'show_dumps') !== true) { + if (! self::allowDump()) { return; } $isConsole = Console::isConsole(); diff --git a/src/Framework/Database/BuildSQL.php b/src/Framework/Database/BuildSQL.php index 049fd79..f16c71c 100644 --- a/src/Framework/Database/BuildSQL.php +++ b/src/Framework/Database/BuildSQL.php @@ -85,4 +85,151 @@ class BuildSQL . "\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; + } } diff --git a/src/Framework/Database/Grammar/Grammar.php b/src/Framework/Database/Grammar/Grammar.php new file mode 100644 index 0000000..7113908 --- /dev/null +++ b/src/Framework/Database/Grammar/Grammar.php @@ -0,0 +1,85 @@ +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; +} \ No newline at end of file diff --git a/src/Framework/Database/Grammar/MySQLGrammar.php b/src/Framework/Database/Grammar/MySQLGrammar.php new file mode 100644 index 0000000..63de94e --- /dev/null +++ b/src/Framework/Database/Grammar/MySQLGrammar.php @@ -0,0 +1,54 @@ +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)}, ?)"; + } +} \ No newline at end of file diff --git a/src/Framework/Database/Grammar/SQLiteGrammar.php b/src/Framework/Database/Grammar/SQLiteGrammar.php new file mode 100644 index 0000000..bf50003 --- /dev/null +++ b/src/Framework/Database/Grammar/SQLiteGrammar.php @@ -0,0 +1,58 @@ +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=? + )"; + } +} \ No newline at end of file diff --git a/src/Framework/Database/Model.php b/src/Framework/Database/Model.php index 36dcab9..980c2d1 100644 --- a/src/Framework/Database/Model.php +++ b/src/Framework/Database/Model.php @@ -22,11 +22,11 @@ class Model { use RunSql; use Validation; - const SUCCESSFUL_SAVE = 1; - const DUPLICATE_FOUND = 2; - const VALIDATION_ERROR = 3; - const PRE_SAVE_FAILED = 4; - const POST_SAVE_FAILED = 5; + public const int SUCCESSFUL_SAVE = 1; + public const int DUPLICATE_FOUND = 2; + public const int VALIDATION_ERROR = 3; + public const int PRE_SAVE_FAILED = 4; + public const int POST_SAVE_FAILED = 5; private $members = []; private $validationMembers = []; @@ -72,6 +72,23 @@ class Model { 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) { echo "
";
print_r($data);
@@ -96,7 +113,8 @@ class Model {
continue;
}
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 {
- $this->members[$member] = $data;
+ $saferMember = $this->sanitizeFieldName($member);
+ $this->members[$saferMember] = $data;
}
public function hasMember($member): bool {
@@ -150,6 +169,30 @@ class Model {
public function clearValidationMember(string $key): void {
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) {
if (!is_array($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')) {
$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->execute(array($id));
if ($query === false) {
@@ -206,55 +285,6 @@ class Model {
}
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 {
return TimeZones::convertTimeZone(['format' => 'database', 'timezone' => 'UTC']);
@@ -281,8 +311,8 @@ class Model {
}
}
- if ($this->hasMember('modified')) {
- $this->setMember('modified', self::makeDbTimeStamp());
+ if ($this->hasMember('updated_at')) {
+ $this->setMember('updated_at', self::makeDbTimeStamp());
}
if (empty($this->members[$this->primaryKey])) {
@@ -293,8 +323,8 @@ class Model {
}
}
- if ($this->hasMember('created')) {
- $this->setMember('created', self::makeDbTimeStamp());
+ if ($this->hasMember('created_at')) {
+ $this->setMember('created_at', self::makeDbTimeStamp());
}
$this->members[$this->primaryKey] = $this->insert($this->table, $this->members);
@@ -315,6 +345,56 @@ class Model {
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.
* So, the KEY does not get exposed!!!!
diff --git a/src/Framework/Database/Paginate.php b/src/Framework/Database/Paginate.php
index ccb5d2b..839b0b3 100644
--- a/src/Framework/Database/Paginate.php
+++ b/src/Framework/Database/Paginate.php
@@ -117,7 +117,13 @@ class Paginate
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
diff --git a/src/Framework/ErrorHandler.php b/src/Framework/ErrorHandler.php
index d6eebb0..f775cec 100644
--- a/src/Framework/ErrorHandler.php
+++ b/src/Framework/ErrorHandler.php
@@ -310,7 +310,7 @@ final class ErrorHandler
$out .= "On Line # " . $e->getLine() . PHP_EOL;
$out .= $e->getTraceAsString() . PHP_EOL;
- echo $color . $out . PHP_EOL;
+ fwrite(STDERR, $color . $out . PHP_EOL);
}
private function getCleanHTML(string $in): string
diff --git a/src/Framework/SaferOutput.php b/src/Framework/SaferOutput.php
index ad039b4..40b24e8 100644
--- a/src/Framework/SaferOutput.php
+++ b/src/Framework/SaferOutput.php
@@ -118,13 +118,18 @@ class SaferOutput
return ($object instanceof \JsonException);
}
- // Escape URL output
+ // Escape URL output by Encoding it
public static function u(string $string): 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
* very powerful, in fact, it does not escape single quotes, cannot detect
* the character set and does not validate HTML as well.
@@ -140,7 +145,7 @@ class SaferOutput
{
return html_entity_decode($data);
}
-
+
/**
* As PHP uses the underlying C functions for file system related operations,
* it may handle null bytes in a quite unexpected way.
diff --git a/src/Framework/Services/Sessions/FileSessionHandler.php b/src/Framework/Services/Sessions/FileSessionHandler.php
index 9212254..cb8a4ba 100644
--- a/src/Framework/Services/Sessions/FileSessionHandler.php
+++ b/src/Framework/Services/Sessions/FileSessionHandler.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
- * @author Robert Strutts
+ * @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
@@ -13,18 +13,18 @@ namespace IOcornerstone\Framework\Services\Sessions;
use IOcornerstone\Framework\GzCompression;
use IOcornerstone\Framework\Enum\CompressionMethod as Method;
-class FileSessionHandler implements SessionHandlerInterface
+class FileSessionHandler implements \SessionHandlerInterface
{
private $savePath;
private $filePrefix;
private $compression;
private $enc;
- public function __construct(false|callable $enc, array $config = [])
+ public function __construct($enc, array $config = [])
{
- $savePath = $config['save_path'] ?: null;
- $filePrefix = $config['prefix'] ?: 'sess_';
+ $savePath = $config['save_path'] ?? null;
+ $filePrefix = $config['prefix'] ?? 'sess_';
$this->filePrefix = $filePrefix;
@@ -36,9 +36,9 @@ class FileSessionHandler implements SessionHandlerInterface
mkdir($this->savePath, 0700, true);
}
- $method = $config['method'] ?: Method::DEFLATE;
- $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);
diff --git a/src/Framework/Services/Sessions/RedisSessionHandler.php b/src/Framework/Services/Sessions/RedisSessionHandler.php
index 014527b..24164cf 100644
--- a/src/Framework/Services/Sessions/RedisSessionHandler.php
+++ b/src/Framework/Services/Sessions/RedisSessionHandler.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
- * @author Robert Strutts
+ * @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
@@ -13,7 +13,7 @@ namespace IOcornerstone\Framework\Services\Sessions;
use IOcornerstone\Framework\GzCompression;
use IOcornerstone\Framework\Enum\CompressionMethod as Method;
-class RedisSessionHandler implements SessionHandlerInterface
+class RedisSessionHandler implements \SessionHandlerInterface
{
private $redis;
private $prefix;
@@ -23,7 +23,7 @@ class RedisSessionHandler implements SessionHandlerInterface
private $lockWait = 100000; // microseconds
private $enc;
- public function __construct(callable $enc, array $config = [])
+ public function __construct($enc, array $config = [])
{
$defaults = [
'host' => '127.0.0.1',
@@ -50,12 +50,26 @@ class RedisSessionHandler implements SessionHandlerInterface
$this->ttl = 1440;
}
- $this->redis = new Redis();
+ $this->redis = new \Redis();
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 {
- $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']) {
@@ -67,16 +81,12 @@ class RedisSessionHandler implements SessionHandlerInterface
}
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']);
+ $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, $config['read_timeout']);
}
- $method = $config['method'] ?: Method::DEFLATE;
- $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);
@@ -135,7 +145,7 @@ class RedisSessionHandler implements SessionHandlerInterface
$data = $this->redis->get($key);
$this->redis->del($key . '.lock');
- return $this->readHelper($data) ?: '';
+ return $this->readHelper($data) ?? '';
}
usleep($this->lockWait);
@@ -147,7 +157,7 @@ class RedisSessionHandler implements SessionHandlerInterface
public function write($sessionId, $data): bool
{
- $data = write_helper($data);
+ $data = $this->writeHelper($data);
$key = $this->prefix . $sessionId;
// Only write if the session has changed
diff --git a/src/Framework/SessionManagement.php b/src/Framework/SessionManagement.php
index 3d499e7..16f41b0 100644
--- a/src/Framework/SessionManagement.php
+++ b/src/Framework/SessionManagement.php
@@ -15,7 +15,6 @@ use IOcornerstone\Framework\Services\Sessions\{
CookieSessionHandler as CookieSes
};
use IOcornerstone\Framework\{
- Common,
Configure,
Registry
};
diff --git a/src/Framework/Trait/Database/QueryBuilder.php b/src/Framework/Trait/Database/QueryBuilder.php
new file mode 100644
index 0000000..fa172ac
--- /dev/null
+++ b/src/Framework/Trait/Database/QueryBuilder.php
@@ -0,0 +1,745 @@
+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();
+ }
+}
diff --git a/src/Framework/Trait/Database/Validation.php b/src/Framework/Trait/Database/Validation.php
index b1986f0..8833ae3 100644
--- a/src/Framework/Trait/Database/Validation.php
+++ b/src/Framework/Trait/Database/Validation.php
@@ -12,7 +12,6 @@ namespace IOCornerstone\Framework\Trait\Database;
use IOcornerstone\Framework\{
Configure,
Common,
- Registry,
Logger,
};
diff --git a/src/Framework/Trait/Security/CsrfTokenFunctions.php b/src/Framework/Trait/Security/CsrfTokenFunctions.php
index 3025fbe..6fcab67 100644
--- a/src/Framework/Trait/Security/CsrfTokenFunctions.php
+++ b/src/Framework/Trait/Security/CsrfTokenFunctions.php
@@ -18,7 +18,8 @@ use IOcornerstone\Framework\{
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
* Useful for JSON data...
@@ -41,13 +42,13 @@ trait CsrfTokenFunctions
public static function csrfTokenTag(): string
{
$token = self::createCsrfToken();
- return "";
+ return "";
}
public static function csrfTokenStillValid(string $csrfTokenKeyName = ""): bool
{
if (empty($csrfTokenKeyName)) {
- $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+ $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
}
$validTimeStamp = self::csrfTokenIsValid($csrfTokenKeyName);
@@ -109,7 +110,7 @@ trait CsrfTokenFunctions
private static function csrfTokenIsValid(string $csrfTokenKeyName = ""): false|int
{
if (empty($csrfTokenKeyName)) {
- $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+ $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
}
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
@@ -142,7 +143,7 @@ trait CsrfTokenFunctions
private static function destroyCsrfToken(string $csrfTokenKeyName = ""): bool
{
if (empty($csrfTokenKeyName)) {
- $csrfTokenKeyName = $_POST['csrf_token'] ?? "";
+ $csrfTokenKeyName = $_POST[self::CSRF_TOKEN_FIELD_NAME] ?? "";
}
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
return false;