From b1715e8f3e31f987ccec725991e6e0fe27ffaf26 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 14 Jun 2026 23:49:37 -0400 Subject: [PATCH] db --- src/Bootstrap.php | 7 + src/Framework/Assets.php | 5 + src/Framework/Common.php | 25 ++ src/Framework/Database/DummyData.php | 105 ++++++ src/Framework/Database/Model.php | 347 +++++++++++++++++++ src/Framework/Database/Paginate.php | 283 +++++++++++++++ src/Framework/HtmlDocument.php | 7 + src/Framework/Http/Kernel.php | 4 +- src/Framework/Http/Request.php | 2 +- src/Framework/Middleware/ErrorMiddleware.php | 8 +- src/Framework/Requires.php | 3 + src/Framework/Security.php | 2 +- src/Framework/SiteHelper.php | 19 +- src/Framework/TagMatches.php | 42 ++- src/Framework/TimeZoneSelection.php | 17 +- src/Framework/View.php | 36 +- 16 files changed, 873 insertions(+), 39 deletions(-) create mode 100644 src/Framework/Database/DummyData.php create mode 100644 src/Framework/Database/Model.php create mode 100644 src/Framework/Database/Paginate.php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 53393bd..1baf824 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -49,6 +49,13 @@ $loader->addNamespace("Psr\Http\Server", [ SiteHelper::setupHTTP(); +function ddd(...$args) { + $stringArgs = array_map(function($arg) { + return (string)$arg; + }, $args); + return dd(implode('', $stringArgs)); +} + function dd($var = 'nothing', endDump $end = endDump::EXIT_AND_STOP) { Common::dump($var, $end); diff --git a/src/Framework/Assets.php b/src/Framework/Assets.php index ffa0ce4..dcdb71a 100644 --- a/src/Framework/Assets.php +++ b/src/Framework/Assets.php @@ -233,6 +233,11 @@ class Assets //return ""; } + public static function inlineCss(string $code): string + { + return "\r\n"; + } + /** * Purpose: To do inline JavaScript. * @param type $code string of code to inline into page. diff --git a/src/Framework/Common.php b/src/Framework/Common.php index e33b6d3..00be7df 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -31,6 +31,31 @@ final class Common { return array_keys($array) !== range(0, count($array) - 1); } + + public static function combineDataToString(object $o, ...$args): string + { + $stringArgs = array_map(function($arg) { + if (is_array($arg) || is_object($arg)) { + $arg = json_encode($arg); + } + + return (string)$arg; + }, $args); + + $seperator = $o->seperator ?? "|"; + $combined = implode($seperator, $stringArgs); + + $useLogger = $o->useLogger ?? false; + if ($useLogger) { + \IOcornerstone\doLogger()->alert($combined); + return $combined; + } + $doDump = $o->doDump ?? false; + if ($doDump) { + dump($combined); + } + return $combined; + } public static function stringSubPart(string $string, int $offset = 0, ?int $length = null, $encoding = null) { if ($length === null) { diff --git a/src/Framework/Database/DummyData.php b/src/Framework/Database/DummyData.php new file mode 100644 index 0000000..fee035a --- /dev/null +++ b/src/Framework/Database/DummyData.php @@ -0,0 +1,105 @@ +pdo = $pdo; + $this->randomEngine = new RandomEngine(); + } + + /* + * Helper for add_dummy_data and get_dummy_data. + * Purpose: To return ONE ROW of junk/dummy data from input data. + * @param array $data + * @retval array of random dummy data AKA one row worth of it. + */ + + private function useDummyData(array $data): array { + $ret = []; + foreach ($data as $field => $array_values) { + $array_count = Common::getCount($array_values); + if ($array_count) { + $ret[$field] = $array_values[$this->randomEngine->getInt(0, $array_count - 1)]; + } + } + return $ret; + } + + /** + * Inserts Dummy Data to DB + * @param int $num_rows to add/make + * @param array $data sample data EX: array('fname'=>array('bob','kim','lisa', ect...), ...) + * @retval bool true + */ + public function addDummyData(string $table, int $num_rows, array $data): bool { + for ($i = 0; $i < $num_rows; $i++) { + $bind_data = $this->useDummyData($data); + $fields = []; + foreach ($data as $field => $array_values) { + $fields[] = $field; + } + + $sql = "INSERT INTO `{$table}` " + . "(" . implode(", ", $fields) . ") " + . "VALUES (:" . implode(", :", $fields) . ");"; + + $bind = []; + foreach ($fields as $field) { + $bind[":$field"] = $bind_data[$field]; + } + unset($bind_data); + unset($fields); + + $pdo_stmt = $this->pdo->prepare($sql); + $exec = $pdo_stmt->execute($bind); + unset($bind); + } + return true; + } + + /** + * Make an array of fields for a fake row of dummy data out of input data. + * @param int $num_rows to make + * @param array $data sample data + * @retval array of rows with dummy data in it. + */ + public function getDummyData(int $num_rows, array $data): array { + if ($num_rows > 100) { + throw new \LengthException("Generating too much dummy data via \$num_rows!"); + } + $ret = []; + for ($i = 0; $i < $num_rows; $i++) { + $ret[] = $this->useDummyData($data); + } + return $ret; + } + + public function getDummyDataGenerator( + int $num_rows, + array $data + ): \Generator { + for ($i = 0; $i < $num_rows; $i++) { + yield $this->useDummyData($data); + } + } + + +} diff --git a/src/Framework/Database/Model.php b/src/Framework/Database/Model.php new file mode 100644 index 0000000..727bc5b --- /dev/null +++ b/src/Framework/Database/Model.php @@ -0,0 +1,347 @@ + + * @copyright Copyright (c) 2022, Robert Strutts. + * @license MIT + */ + +namespace IOcornerstone\Framework\Database; + +use IOcornerstone\Framework\{ + TimeZones, + Common, +}; + +class Model { + + use \IOCornerstone\Framework\Trait\Database\RunSql; + use \IOCornerstone\Framework\Trait\Database\Validation; + + const SUCCESSFUL_SAVE = 1; + const DUPLICATE_FOUND = 2; + const VALIDATION_ERROR = 3; + const PRE_SAVE_FAILED = 4; + const POST_SAVE_FAILED = 5; + + private $members = []; + private $validationMembers = []; + private $missing = []; + private $errorMessage; + private $primaryKey = 'id'; + private $dbSkiped = []; + + public function saved(bool $worked = true): array { + $time = TimeZones::convertTimeZone(array('format' => 'g:i a')); + return ['time' => $time, 'saved' => $worked]; + } + + /** + * JSON API inform AJAX that save failed + */ + public function saveFailed($msg = 'Save Failed!'): array { + return ['saved' => false, 'msg' => $msg]; + } + + public function dumpTableFields(string $table) { + $fields = $this->get_fields($table); + $this->doDump($fields); + } + + public function dumpDiff(): bool { + $fields = $this->get_fields($this->table); + + $diff = array_diff($fields, array_keys($this->members)); + + if (($key = array_search('id', $diff)) !== false) { + unset($diff[$key]); // Who cares about IDs + } + + foreach($this->dbSkiped as $skip) { + unset($diff[$skip]); + } + + if (count($diff)) { + echo "Diff on fields:
" . PHP_EOL; + $this->doDump($diff); + return true; + } + return false; + } + + private function doDump(array $data) { + echo "
";
+        print_r($data);
+        echo '
'; + } + + public function setPrimaryKeyName(string $key): void { + $this->primaryKey = $key; + } + + /** + * Do not use this one in production! AS anyone can override + * the DB security and Inject stuff into the DB via POST, etc... + */ + public function autoSetMembers(array $globals = [], array $skip = ['route', 'm'], array $onlyThese = []): void { + if (isLive()) { + return; // Bail if LIVE + } + + foreach ($globals as $key => $data) { + if (count($skip) && in_array($key, $skip)) { + continue; + } + if (in_array($key, $onlyThese) || !count($onlyThese)) { + $this->members[$key] = (! empty($data)) ? $data : ""; + } + } + } + + public function getMissing() { + return $this->missing; + } + + public function getMember(string $member) { + return $this->members[$member] ?? null; + } + + public function getMembers() { + return $this->members; + } + + public function setMember(string $member, string $data): void { + $this->members[$member] = $data; + } + + public function hasMember($member): bool { + return isset($this->members[$member]); + } + + public function getLastError(): string { + return $this->errorMessage; + } + + /** + * Set Member for Validation + * @param string $key + * @param array $a + */ + public function setMemberForValidation(string $key, array $a): void { + $this->validationMembers[$key] = $a; + } + + /** + * Get Validation Member + * @param string $key + * @return array + */ + public function getVaildationMember(string $key): array { + return $this->validationMembers[$key] ?? array(); + } + + /** + * Unset Validation Member + * @param string $key + */ + public function clearValidationMember(string $key): void { + unset($this->validationMembers[$key]); + } + + private function cleanup($bind) { + if (!is_array($bind)) { + if (!empty($bind)) { + $bind = array($bind); + } else { + $bind = array(); + } + } + return $bind; + } + + public static function emptyGenerator(): \Generator { + yield from []; + } + + /** + * Please use this fetch_lazy instead of fetch_all!!! + * To AVOID running out of Memory!!!!!!!!!!!!!!!!!!!! + * @param \PDOStatement $stmt + * @return \Generator + */ + public static function pdoFetchLazy(\PDOStatement $stmt): \Generator { + foreach($stmt as $record) { + yield $record; + } + } + + public static function isValid(array $data): bool { + $error_count = (isset($data['errors'])) ? count($data['errors']) : 0; + return ($error_count === 0); + } + + + public function load(int $id): bool{ + if (method_exists($this, 'pre_load')) { + $this->pre_load(); + } + + $sql = "SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1"; + $query = $this->pdo->prepare($sql); + $query->execute(array($id)); + if ($query === false) { + return false; + } + $data = $query->fetch(\PDO::FETCH_ASSOC); + if ($data === false) { + return false; + } + $this->members = array_merge($this->members, $data); + + if (method_exists($this, 'post_load')) { + $this->post_load(); + } + 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']); + } + + /** + * Save + * + * @param object $lcoal_model Should be $this from the Model + * + * Insert if primary key not set + * Update if primary key set + */ + public function save(bool $validate = true): int { + if (method_exists($this, 'preSave')) { + $preWasSuccessfull = $this->preSave(); + if (! $preWasSuccessfull) { + return self::PRE_SAVE_FAILED; + } + } + $name = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); // Get DB Driver + if ($name === 'mysql' && $validate) { + if (!$this->validateMysql()) { + return self::VALIDATION_ERROR; + } + } + + if ($this->hasMember('modified')) { + $this->setMember('modified', self::makeDbTimeStamp()); + } + + if (empty($this->members[$this->primaryKey])) { + if (method_exists($this, 'duplicateCheck')) { + $dup = $this->duplicateCheck(); + if ($dup) { + return self::DUPLICATE_FOUND; + } + } + + if ($this->hasMember('created')) { + $this->setMember('created', self::makeDbTimeStamp()); + } + + $this->members[$this->primaryKey] = $this->insert($this->table, $this->members); + } else { + $this->setMember("row_count", $this->update($this->table, $this->members, + "`{$this->primaryKey}` = :search_key", + array(':search_key' => $this->members[$this->primaryKey]) + )); + } + + if (method_exists($this, 'postSave')) { + $postSaveSuccessfull = $this->postSave(); + if (! $postSaveSuccessfull) { + return self::POST_SAVE_FAILED; + } + } + + return self::SUCCESSFUL_SAVE; + } + + /** + * This FN requires SSL connection to the database. + * So, the KEY does not get exposed!!!! + */ + public function encodeMySQLusingAES(#[\SensitiveParameter] string $data, #[\SensitiveParameter] 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}'))"; + Common::wipe($key); + Common::wipe($safe_key); + Common::wipe($data); + Common::wipe($safe_text); + Common::wipe($parm); + return $ret; + } + + /** + * This FN requires SSL connection to the database. + * So, the KEY does not get exposed!!!! + */ + public function decodeMySQLusingAES(string $field_name, #[\SensitiveParameter] 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 */ +} diff --git a/src/Framework/Database/Paginate.php b/src/Framework/Database/Paginate.php new file mode 100644 index 0000000..ccb5d2b --- /dev/null +++ b/src/Framework/Database/Paginate.php @@ -0,0 +1,283 @@ +_conn = $conn; + $this->_query = $query; + + if ($dbType === "mysql") { + $rs = $this->_conn->query($this->_query); + $this->_total = $rs->rowCount(); + } + } + + private function setLimit(int $limit = 10): int + { + if ($limit < 1) { + return 10; + } + return ($limit > $this->maxLimit) ? $this->maxLimit : $limit; + } + + private function setPage(int $page = 1): int + { + return ($page < 1) ? 1 : $page; + } + + public function mongoGetData(int $limit = 10, int $page = 1, array $options = []) + { + $this->_limit = $this->setLimit($limit); // Number of items per page + $this->_page = $this->setPage($page); // The current page number + + $skip = (($this->_page - 1) * $this->_limit); + + if ($this->_limit === 0) { + $db_options = $options; + } else { + $limits = ['limit' => $this->_limit, 'skip' => $skip]; + $db_options = array_merge($limits, $options); + } + + // NOTE _query is the WHERE condition as an array + $collection = $this->_conn; + $documents = $collection->find($this->_query, $db_options); + + // Calculate the total number of documents in the collection + $this->_total = $collection->countDocuments($this->_query); + + $result = new \stdClass(); + $result->page = $this->_page; + $result->limit = $this->_limit; + $result->total = $this->_total; + $result->data = $documents; + + return $result; + } + + public function getData(int $limit = 10, int $page = 1) + { + $this->_limit = $this->setLimit($limit); + $this->_page = $this->setPage($page); + + if ($this->_limit === 0) { + $query = $this->_query; + } else { + $query = $this->_query . " LIMIT " . (($this->_page - 1) * $this->_limit) . ", $this->_limit"; + } + $rs = $this->_conn->query($query); + + $result = new \stdClass(); + $result->page = $this->_page; + $result->limit = $this->_limit; + $result->total = $this->_total; + $result->rs = $rs; + + return $result; + } + + public function useHashRoutes(string $url): void + { + $this->_url = $url; + $this->_url_limit = "/"; + $this->_url_page = "/"; + } + + public function useGetRoutes(string $url = ""): void + { + $this->_url = $url; + $this->_url_limit = "?limit="; + $this->_url_page = "&page="; + } + + private function do_href(int $limit, int $page): string + { + return 'href="' . $this->_url . $this->_url_limit . $limit . $this->_url_page . $page . '"'; + } + + public function createLinks(int $links = 7, string $list_class = "ui pagination menu", string $item = "item"): string + { + $last = ceil($this->_total / $this->_limit); + + if ($this->_limit === 0 || $last < 2) { + return ''; + } + + $start = (($this->_page - $links) > 0) ? $this->_page - $links : 1; + $end = (($this->_page + $links) < $last) ? $this->_page + $links : $last; + + $html = '
'; + + $class = ($this->_page == 1) ? "disabled" : ""; + $item = " " . $item; + + $html .= 'do_href($this->_limit, $this->_page - 1) . '>«'; + + if ($start > 1) { + $html .= 'do_href($this->_limit, 1) . '>1'; + $html .= '
...
'; + } + + for ($i = $start; $i <= $end; $i++) { + $class = ($this->_page == $i) ? "active" : ""; + $html .= 'do_href($this->_limit, $i) . '>' . $i . ''; + } + + if ($end < $last) { + $html .= '
...
'; + $html .= 'do_href($this->_limit, $last) . '>' . $last . ''; + } + + $class = ($this->_page == $last) ? "disabled" : ""; + $html .= 'do_href($this->_limit, $this->_page + 1) . '>»'; + + $html .= '
'; + + return $html; + } + + public function createJumpMenuWithLinks(int $links = 7, string $label = "Jump to ", string $end_label = " page.", string $item = "item"): string + { + if ($this->_limit === 0) { + return ''; + } + + $last = ceil($this->_total / $this->_limit); + + $start = (($this->_page - $links) > 0) ? $this->_page - $links : 1; + $end = (($this->_page + $links) < $last) ? $this->_page + $links : $last; + + // Prev. Page + $class = ($this->_page == 1) ? "disabled " : ""; + $item = " " . $item; + $href = ($this->_page == 1) ? "" : $this->do_href($this->_limit, $this->_page - 1); + $html .= '«'; + + $html .= $this->createJumpMenu(); + + // Next Page + $class = ($this->_page == $last) ? "disabled " : ""; + $href = ($this->_page == $last) ? "" : $this->do_href($this->_limit, $this->_page + 1); + $html .= '»'; + + return $html; + } + + public function createJumpMenu(string $label = "Jump to ", string $end_label = " page.", string $item = "item"): string + { + $last = ceil($this->_total / $this->_limit); + $option = ''; + for ($i = 1; $i <= $last; $i++) { + $option .= ($i == $this->_page) ? "\n" : "\n"; + } + return "\n"; + } + + public function createItemsPerPage(string $label = "Items ", string $end_label = " per page.", string $item = "item"): string + { + $items = ''; + $ipp_array = array(3, 6, 12, 24, 50, 100); + $found = false; + foreach ($ipp_array as $ipp_opt) { + if ($ipp_opt == $this->_limit) { + $found = true; + } + $items .= ($ipp_opt == $this->_limit) ? "\n" : "\n"; + } + + if ($found === false) { + $items = "\n" . $items; + } + + $my_page = str_replace("&", "?", $this->_url_page); + $my_limit = str_replace("?", "&", $this->_url_limit); + + return "\n"; + } + + public function getCSS(): string + { + return ".pagination a { + color: #333; + padding: 2px 20px; + line-height: 1; + font-size: 14px; + text-decoration: none; + border-radius: 6px; + border: 1px solid #ddd; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,.1); + transition: all .2s ease; +} +.pagination a:hover:not(.active) { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(0,0,0,.15); +} +.pagination a.active { + background: #4CAF50; + color: white; + border-color: #4CAF50; +} +.pagination .disabled { + display: inline-block; + padding: 4px 12px; + color: #999; + border: 1px solid #ddd; + border-radius: 4px; + background: #f8f8f8; + cursor: not-allowed; + pointer-events: none; + opacity: 0.6; +} +div.pagination.menu { + padding-top: 10px; +}"; + } +} + +/* + * mongo db + $limit = $_GET['limit'] ?? 18; + $page = $_GET['page'] ?? 1; + $url = "#pages/bill"; + $query = [ 'user_id' => (string) $user_id ]; + $mongo_options = [ 'projection' => [ 'billing' => 1 ]]; + + $pag = new Paginate($collection, $query, "mongodb"); + $r = $pag->mongoGetData($limit, $page, $mongo_options); + $pag->useHashRoutes($url); + + foreach ($r->data as $bill) { + echo $bill; + } + + echo $pag->createLinks(); + */ \ No newline at end of file diff --git a/src/Framework/HtmlDocument.php b/src/Framework/HtmlDocument.php index c6028a6..096e584 100644 --- a/src/Framework/HtmlDocument.php +++ b/src/Framework/HtmlDocument.php @@ -352,6 +352,13 @@ class HtmlDocument } } + public function addToCss(string $css): void + { + if (!empty($css)) { + $this->styles .= Assets::inlineCss($css); + } + } + /** * Use CSS/JS for Database SSP public function datatables_code(): void { diff --git a/src/Framework/Http/Kernel.php b/src/Framework/Http/Kernel.php index d1c05ba..bc5d04b 100644 --- a/src/Framework/Http/Kernel.php +++ b/src/Framework/Http/Kernel.php @@ -121,7 +121,9 @@ class Kernel { private function emit(Response $response): void { - http_response_code($response->getStatusCode()); + if (! Console::isConsole()) { + http_response_code($response->getStatusCode()); + } foreach ($response->getHeaders() as $name => $values) { if (! is_array($values) && ! is_object($values)) { header("$name: $values", false); diff --git a/src/Framework/Http/Request.php b/src/Framework/Http/Request.php index b6eaddf..ed2b87f 100644 --- a/src/Framework/Http/Request.php +++ b/src/Framework/Http/Request.php @@ -6,7 +6,7 @@ use Psr\Http\Message\{ ServerRequestInterface, UriInterface, StreamInterface, - UploadedFileInterface +// UploadedFileInterface }; use IOcornerstone\Framework\Trait\ServerRequestDelegation; use IOcornerstone\Framework\ParameterBag; diff --git a/src/Framework/Middleware/ErrorMiddleware.php b/src/Framework/Middleware/ErrorMiddleware.php index 82217f5..62cbbb7 100644 --- a/src/Framework/Middleware/ErrorMiddleware.php +++ b/src/Framework/Middleware/ErrorMiddleware.php @@ -19,6 +19,7 @@ use IOcornerstone\Framework\{ final class ErrorMiddleware implements MiddlewareInterface { + private $headers = []; public function __construct( private LoggerInterface $logger, private bool $hideErrors = false @@ -43,8 +44,7 @@ final class ErrorMiddleware implements MiddlewareInterface : 'Internal Server Error'; $stream = Stream::fromString($bodyString); - - return new Response(500, ['Content-Type' => 'text/plain'], $stream); + return new Response(status: 500, body: $stream, headers: $this->headers); } } @@ -62,6 +62,7 @@ final class ErrorMiddleware implements MiddlewareInterface if (Console::isConsole()) { $codeNumber = $e->getCode() ?? 0; + $this->headers = ['Content-Type' => 'text/plain']; return sprintf( "Code# %d; %s\n\n%s", $codeNumber, @@ -71,9 +72,10 @@ final class ErrorMiddleware implements MiddlewareInterface } if (Reg::get('error_handler')->isJsonRequest()) { + $this->headers = ['Content-Type' => 'application/json']; Reg::get('error_handler')->getJsonDebug($e); } - + $this->headers = ['Content-Type' => 'text/html']; return Reg::get('error_handler')->formatWebMessage($e); } } diff --git a/src/Framework/Requires.php b/src/Framework/Requires.php index e1f9001..1a76c8c 100644 --- a/src/Framework/Requires.php +++ b/src/Framework/Requires.php @@ -15,6 +15,7 @@ enum UseDir: string { case FRAMEWORK = "Framework"; case PROJECT = "Project"; case ONERROR = "OnError"; + case BASEDIR = "BaseDir"; } final class Requires { @@ -132,6 +133,7 @@ final class Requires { // Keep offset negitive, to get file kind... $fileKind = self::stringSubPart($file, -(strlen($file) - $posLastOccurrence)); $fileType = match ($fileKind) { + ".txt" => ".txt", ".twig" => ".twig", ".tpl" => ".tpl", default => ".php", @@ -160,6 +162,7 @@ final class Requires { private static function secureFile(bool $returnContents, string $file, UseDir $path, $local = null, array $args = array(), bool $loadOnce = true) { $dir = match ($path) { UseDir::FIXED => "", + UseDir::BASEDIR => BaseDir, UseDir::FRAMEWORK => IO_CORNERSTONE_FRAMEWORK, UseDir::ONERROR => IO_CORNERSTONE_PROJECT . "Views/OnError/", default => IO_CORNERSTONE_PROJECT, diff --git a/src/Framework/Security.php b/src/Framework/Security.php index 3f283a9..7d653e5 100644 --- a/src/Framework/Security.php +++ b/src/Framework/Security.php @@ -119,7 +119,7 @@ class Security * salt and prevents time based side-channel attacks. */ - public static function do_password_hash(#[\SensitiveParameter] string $password): bool|string + public static function doPasswordHash(#[\SensitiveParameter] string $password): bool|string { $pwdPeppered = self::makeHash($password); $hashAlgo = Configure::get( diff --git a/src/Framework/SiteHelper.php b/src/Framework/SiteHelper.php index 7dea856..30831a8 100644 --- a/src/Framework/SiteHelper.php +++ b/src/Framework/SiteHelper.php @@ -10,8 +10,10 @@ declare(strict_types=1); namespace IOcornerstone\Framework; -use IOcornerstone\Framework\Security; - +use IOcornerstone\Framework\{ + Security, + Console, +}; /** * Description of SiteHelper * Checks if IP is allowed for LIVE DEBUGGING @@ -100,6 +102,19 @@ final class SiteHelper public static function remoteNotAllowedForceLive(): bool { + if (Console::isConsole()) { + return false; // false to show errors and dumps + } + + $s = $_SESSION['usersRights'] ?? false; + if ($s !== false && strlen($s) > 4) { + $rights = json_decode($s, associative: true); + $flipped = array_flip($rights); + if (isset($flipped['developer'])) { + return false; // false for Developers to see Errors/Logs + } + } + return (!self::is_allowed()); } diff --git a/src/Framework/TagMatches.php b/src/Framework/TagMatches.php index 25d838a..8942f82 100644 --- a/src/Framework/TagMatches.php +++ b/src/Framework/TagMatches.php @@ -15,7 +15,9 @@ use IOcornerstone\Framework\String as SF; final class TagMatches { - const TAGS_TO_CHECK = array('div', 'span', 'form', 'i*', 'a*', 'h1', 'p*'); + const TAGS_TO_CHECK = ['div', 'span', 'form', + 'i*', 'a*', 'h?', 'p*', 'td', 'th*', 'tr' + ]; /** * Function checks tags to make sure they match. @@ -23,40 +25,50 @@ final class TagMatches * @param string $page * @return array [output, alert] */ - public static function checkTags(string $page): array + public static function checkTags( + string $page, + array $tags = [], + object $objAlerts + ): array { + $array_of_tags = array_merge(self::TAGS_TO_CHECK, $tags); $alert = ''; $output = ''; $lowercasePage = SF\StringFacade::strtolower($page); unset($page); - - $assets = "/assets/uikit/css/uikit.gradient.min.css"; - $ui = ''; - $ui .= '
'; + $ui = ''; + $ui .= '
'; $ui_end = '
'; - foreach (self::TAGS_TO_CHECK as $tagName) { - if (str_contains($tagName, '*')) { - $tagName = str_replace('*', '', $tagName); - $otag = "<{$tagName}>"; // Open Tag + foreach ($array_of_tags as $tagName) { + $slen = strlen($tagName); + $tagNameWithoutStar = str_replace('*', '', $tagName); + $justTheTag = str_replace('?', '', $tagNameWithoutStar); + if (substr($tagName, $slen - 1, 1) === '*') { + $otag = "<{$justTheTag}>"; // Open Tag $open = substr_count($lowercasePage, $otag); // Count open tags in page - $otag = "<{$tagName} "; /* Open Tag with space */ + $otag = "<{$justTheTag} "; /* Open Tag with space */ $open += substr_count($lowercasePage, $otag); // Count open tags in page } else { - $otag = "<{$tagName}"; // Open Tag + $otag = "<{$justTheTag}"; // Open Tag $open = substr_count($lowercasePage, $otag); // Count open tags in page } - $ctag = ""; // Close Tag + if (substr($tagName, $slen - 1, 1) === '?') { + $ctag = ""; // Close Tag + } + $closed = substr_count($lowercasePage, $ctag); // Count Close tags in page $totalStillOpen = $open - $closed; // Difference of open vs. closed.... if ($totalStillOpen > 0) { - $msg = "{$totalStillOpen} possibly MISSING closing {$tagName} !!!"; + $msg = "{$totalStillOpen} possibly MISSING closing {$justTheTag} Tags!!!"; $alert .= "console.log('{$msg}');\r\n"; $output .= (isLive()) ? "\r\n" : "{$ui}{$msg}{$ui_end}\r\n"; } elseif ($totalStillOpen < 0) { - $msg = abs($totalStillOpen) . " possibly MISSING opening {$tagName} !!!"; + $msg = abs($totalStillOpen) . " possibly MISSING opening {$justTheTag} Tags!!!"; $alert .= "console.log('{$msg}');\r\n"; $output .= (isLive()) ? "\r\n" : "{$ui}{$msg}{$ui_end}\r\n"; } diff --git a/src/Framework/TimeZoneSelection.php b/src/Framework/TimeZoneSelection.php index 6d2044d..08ae90a 100644 --- a/src/Framework/TimeZoneSelection.php +++ b/src/Framework/TimeZoneSelection.php @@ -29,20 +29,23 @@ class TimeZoneSelection foreach ($regions as $name => $mask) { $zones = \DateTimeZone::listIdentifiers($mask); foreach ($zones as $timezone) { - $this->timezones[$name][$timezone] = substr($timezone, strlen($name) + 1); + $time = new \DateTime("now", new \DateTimeZone($timezone)); + $this->timezones[$name][$timezone] = substr($timezone, strlen($name) + 1) . ' - ' . $time->format('g:i a'); } } } - public function view() { - echo '
'; foreach ($this->timezones as $region => $list) { - echo '' . "\n"; + $ret .= '' . "\n"; foreach ($list as $timezone => $name) { - echo '' . "\n"; + $selected = ($tzName === $timezone) ? "Selected" : ""; + $ret .= '' . "\n"; } - echo '' . "\n"; + $ret .= '' . "\n"; } - echo ''; + $ret .= ''; + return $ret; } } diff --git a/src/Framework/View.php b/src/Framework/View.php index 85143fc..bbedcdb 100644 --- a/src/Framework/View.php +++ b/src/Framework/View.php @@ -21,15 +21,18 @@ use IOcornerstone\Framework\{ final class View { - public $whiteSpaceControl = false; - public $pageOutput; - private $vars = []; - private $files = []; + public bool $whiteSpaceControl = false; + public string $pageOutput; + private array $vars = []; + private array $files = []; + private array $tags = []; + private string $alertAssets = "/assets/uikit/css/uikit.gradient.min.css"; + private string $alertClass = "uk-alert uk-alert-danger"; private $template = false; - private $useTemplateEngine = false; - private $useTemplateEngineTwig = false; - private $useTemplateEngineLiquid = false; - private $templateType = 'tpl'; + private bool $useTemplateEngine = false; + private bool $useTemplateEngineTwig = false; + private bool $useTemplateEngineLiquid = false; + private string $templateType = 'tpl'; protected $tempalteEngineTwig = null; protected $tempalteEngineLiquid = null; @@ -167,6 +170,17 @@ final class View } } + public function changeAlerts(string $alertAsset, string $alertClass): void + { + $this->alertAssets = $alertAsset; + $this->alertClass = $alertClass; + } + + public function addTags(array $tags): void + { + $this->tags = $tags; + } + /** * Sets a variable in this view with the given name and value * @@ -259,7 +273,11 @@ final class View $pageOutput .= ob_get_clean(); if (Configure::get('IOcornerstone', 'check_HTML_tags') === true) { - $tags = TagMatches::checkTags($pageOutput); + $obj = new \stdClass(); + $obj->assets = $this->alertAssets; + $obj->class = $this->alertClass; + + $tags = TagMatches::checkTags($pageOutput, $this->tags, $obj); if (!empty($tags['output'])) { $pageOutput .= $tags['output']; $pageOutput .= '';