diff --git a/src/Framework/Common.php b/src/Framework/Common.php index 00be7df..3927717 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace IOcornerstone\Framework; -use IOcornerstone\Framework\Enum\ExitOnDump as endDump; -use IOcornerstone\Framework\Console; -use IOcornerstone\Framework\String\StringFacade as F; +use IOcornerstone\Framework\{ + Console, + Logger, + String\StringFacade as F, + Enum\ExitOnDump as endDump, +}; final class Common { @@ -47,7 +50,8 @@ final class Common $useLogger = $o->useLogger ?? false; if ($useLogger) { - \IOcornerstone\doLogger()->alert($combined); + $logger = new Logger(); + $logger->alert($combined); return $combined; } $doDump = $o->doDump ?? false; diff --git a/src/Framework/ErrorHandler.php b/src/Framework/ErrorHandler.php index 6b07bb3..d6eebb0 100644 --- a/src/Framework/ErrorHandler.php +++ b/src/Framework/ErrorHandler.php @@ -96,6 +96,8 @@ final class ErrorHandler $e = new ErrorException('Unknown error'); } + $this->logException($e); + if ($this->isJsonRequest()) { if ($this->debug) { $this->renderJsonDebug($e); @@ -110,7 +112,6 @@ final class ErrorHandler $this->renderConsole($e); } else { $this->renderProductionConsole($e); - $this->logException($e); } return true; } @@ -123,7 +124,6 @@ final class ErrorHandler $this->renderDebug($e); } else { $this->renderProduction($e); - $this->logException($e); } // Don't execute PHP's internal error handler return true; @@ -163,7 +163,7 @@ final class ErrorHandler ], true); } - private function getErrorType(Throwable $e): string + public function getErrorType(Throwable $e): string { if ($e instanceof ErrorException) { return match ($e->getSeverity()) { @@ -313,6 +313,33 @@ final class ErrorHandler echo $color . $out . PHP_EOL; } + private function getCleanHTML(string $in): string + { + return htmlspecialchars($in, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8'); + } + + /** + * Helper FN for ErrorMiddleware + * @param Throwable $e + * @return string + */ + public function getCodeMessage(Throwable $e): string + { + $styles = [ + 'error' => 'uk-alert-danger', + 'warning' => 'uk-alert-warning', + 'notice' => 'uk-alert-primary' + ]; + $type = $this->colorFromType($e); + $style = $styles[$type] ?? $styles['error']; + $ret = "
" . PHP_EOL; + $ret .= " File: " . $this->getCleanHTML($e->getFile()) . PHP_EOL + . "LINE# " . $e->getLine() . PHP_EOL . $this->getCleanHTML($e->getMessage()) . PHP_EOL . $this->getCleanHTML($e->getTraceAsString()); + $ret .= "
" . PHP_EOL; + $ret .= "
" . PHP_EOL; + return $ret; + } + public function getCodeNumber(Throwable $e): string { $dCode = $e->getCode() ?? 0; @@ -387,6 +414,7 @@ final class ErrorHandler private function setLoggerByLevel(Throwable $e): void { $errorMessage = (string) $e; + $logger = new Logger(); if ($logger === false) { return; diff --git a/src/Framework/Logger.php b/src/Framework/Logger.php index a52bb61..9de0164 100644 --- a/src/Framework/Logger.php +++ b/src/Framework/Logger.php @@ -11,39 +11,39 @@ use Psr\Log\InvalidArgumentException; final class Logger implements LoggerInterface { + const LOGS_DIR = '/protected/logs'; + const LOGS_EXT = '.log.txt'; + const LOG_RETETION_DAYS = 90; // Keep 3 Months of LOGS + private $handle = false; - private string $minLevel; private const LEVEL_PRIORITY = [ LogLevel::EMERGENCY => 600, - LogLevel::ALERT => 550, - LogLevel::CRITICAL => 500, - LogLevel::ERROR => 400, - LogLevel::WARNING => 300, - LogLevel::NOTICE => 250, - LogLevel::INFO => 200, - LogLevel::DEBUG => 100, + LogLevel::ALERT => 550, + LogLevel::CRITICAL => 500, + LogLevel::ERROR => 400, + LogLevel::WARNING => 300, + LogLevel::NOTICE => 250, + LogLevel::INFO => 200, + LogLevel::DEBUG => 100, ]; - private const ENV_LEVEL_MAP = [ - 'production' => LogLevel::ERROR, - 'prod' => LogLevel::ERROR, - - 'staging' => LogLevel::WARNING, - - 'testing' => LogLevel::NOTICE, - + 'production' => LogLevel::ERROR, + 'prod' => LogLevel::ERROR, + 'staging' => LogLevel::WARNING, + 'testing' => LogLevel::NOTICE, 'development' => LogLevel::DEBUG, - 'dev' => LogLevel::DEBUG, - 'local' => LogLevel::DEBUG, + 'dev' => LogLevel::DEBUG, + 'local' => LogLevel::DEBUG, ]; public function __construct( - string $filename = 'system', - int $maxCount = 1000, - ?string $minLevel = null - ) { + string $filename = 'system', + int $maxCount = 1000, + ?string $minLevel = null + ) + { $env = self::detectEnv(); if ($minLevel === null) { @@ -53,7 +53,7 @@ final class Logger implements LoggerInterface if (!isset(self::LEVEL_PRIORITY[$minLevel])) { throw new InvalidArgumentException('Invalid log level: ' . $minLevel); } - + $this->minLevel = $minLevel; // Stop Up Level attacks @@ -61,12 +61,12 @@ final class Logger implements LoggerInterface return false; } - if (! defined("BaseDir")) { + if (!defined("BaseDir")) { return false; } - $logDir = Requires::filterDirPath(BaseDir . '/protected/logs'); - if (! Requires::isValidFile($logDir)) { + $logDir = Requires::filterDirPath(BaseDir . self::LOGS_DIR); + if (!Requires::isValidFile($logDir)) { return false; } @@ -74,20 +74,20 @@ final class Logger implements LoggerInterface mkdir($logDir, 0775, true); } - if (! Requires::isValidFile($filename)) { + if (!Requires::isValidFile($filename)) { return false; } $safeFileName = Requires::filterFileName($filename); if ($safeFileName === false || $safeFileName === "") { return false; } - $file = $logDir . '/' . $safeFileName . '.log.txt'; + $file = $logDir . '/' . $safeFileName . self::LOGS_EXT; - if ($maxCount > 1 && $this->getLines($file) > $maxCount) { - @unlink($file); + if ($maxCount > 5 && $this->getLines($file) > $maxCount) { + $aLogs = $this->rotateLog($file, $safeFileName); } - if (! file_exists($file)) { + if (!file_exists($file)) { if (file_put_contents($file, "\n", FILE_APPEND) === false) { return false; } @@ -100,20 +100,66 @@ final class Logger implements LoggerInterface } $this->handle = fopen($file, 'ab'); + + if (isset($aLogs)) { + $this->alert("Purged from Logs = " . $aLogs['logsDeleted']); + } return true; } + private function cleanupLogs($days): int + { + // Safety check... + if ($days < 2) { + return 0; + } + $logDir = Requires::filterDirPath(BaseDir . self::LOGS_DIR); + if ($logDir === false) { + return 0; // DIR unsafe... + } + $cutoff = strtotime("-$days days"); + $files = glob($logDir . "/*" . self::LOGS_EXT); + $count = 0; + + foreach ($files as $file) { + if (filemtime($file) < $cutoff) { + if (unlink($file)) { + $count++; + } + } + } + return $count; + } + + private function rotateLog(string $logFile, string $safeFileName): array + { + $directory = dirname($logFile); + + // Create new filename with current date, hour, and minute + $newFileName = sprintf( + '%s_%s%s', + $safeFileName, + date('Y-m-d_H-i'), // Format: 2026-06-15_14-30 + self::LOGS_EXT + ); + + $newFile= $directory . DIRECTORY_SEPARATOR . $newFileName; + $success = @rename($logFile, $newFile); + + $deleteCount = $this->cleanupLogs(self::LOG_RETETION_DAYS); + return ['renamedSuccess' => $success, 'logsDeleted' => $deleteCount]; + } + /* ---------------- PSR-3 Core ---------------- */ - public function log($level, $message, array $context = []): void + public function log($level, $message, array $context = [], string $details = ""): void { if (!is_string($level) || !isset(self::LEVEL_PRIORITY[$level])) { throw new InvalidArgumentException('Invalid log level'); } - + if ( - self::LEVEL_PRIORITY[$level] - < self::LEVEL_PRIORITY[$this->minLevel] + self::LEVEL_PRIORITY[$level] < self::LEVEL_PRIORITY[$this->minLevel] ) { return; } @@ -122,12 +168,19 @@ final class Logger implements LoggerInterface return; } - $message = $this->interpolate((string)$message, $context); + $message = $this->interpolate((string) $message, $context); + + if (!empty($details)) { + $customeLevel = strtoupper($details); + $message = "Default level was: " . strtoupper($level) . " -- " . $message; + } else { + $customeLevel = strtoupper($level); + } $time = (new \DateTimeImmutable())->format('Y-m-d H:i:s'); fwrite( - $this->handle, - sprintf("[%s] %s: %s\n", $time, strtoupper($level), $message) + $this->handle, + sprintf("[%s] %s: %s\n", $time, strtoupper($customeLevel), $message) ); } @@ -148,6 +201,11 @@ final class Logger implements LoggerInterface $this->log(LogLevel::CRITICAL, $message, $context); } + public function SomeSystemError(string $level, $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context, $level); + } + public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); @@ -174,21 +232,21 @@ final class Logger implements LoggerInterface } /* ---------------- Helpers ---------------- */ + private static function detectEnv(): string { return strtolower( - getenv('APP_ENV') - ?: ($_ENV['APP_ENV'] ?? 'production') + getenv('APP_ENV') ?: ($_ENV['APP_ENV'] ?? 'production') ); } - + private function interpolate(string $message, array $context): string { $replace = []; foreach ($context as $key => $value) { if (is_scalar($value) || $value instanceof \Stringable) { - $replace['{' . $key . '}'] = (string)$value; + $replace['{' . $key . '}'] = (string) $value; } } diff --git a/src/Framework/Middleware/ErrorMiddleware.php b/src/Framework/Middleware/ErrorMiddleware.php index 62cbbb7..2a9acae 100644 --- a/src/Framework/Middleware/ErrorMiddleware.php +++ b/src/Framework/Middleware/ErrorMiddleware.php @@ -32,9 +32,13 @@ final class ErrorMiddleware implements MiddlewareInterface try { return $handler->handle($request); } catch (\Throwable $e) { + $sLevel = Reg::get('error_handler')->getErrorType($e); $codeNumber = Reg::get('error_handler')->getCodeNumber($e); - $message = $codeNumber . $e->getMessage(); - $this->logger->error( + $niceErrors = Reg::get('error_handler')->getCodeMessage($e); + $message = $codeNumber . $niceErrors; + + $this->logger->SomeSystemError( + $sLevel, $message, ['exception' => $e] ); diff --git a/src/Framework/Security.php b/src/Framework/Security.php index 7d653e5..d7f7e37 100644 --- a/src/Framework/Security.php +++ b/src/Framework/Security.php @@ -328,6 +328,19 @@ class Security return $ipaddress; } + public static function doesStringContainsPhp(string $data): bool + { + $pos = strpos($data, ''; foreach ($array_of_tags as $tagName) { - $slen = strlen($tagName); - $tagNameWithoutStar = str_replace('*', '', $tagName); - $justTheTag = str_replace('?', '', $tagNameWithoutStar); - if (substr($tagName, $slen - 1, 1) === '*') { + $justTheTag = str_replace('*', '', $tagName); + if (str_contains($tagName, "*")) { $otag = "<{$justTheTag}>"; // Open Tag $open = substr_count($lowercasePage, $otag); // Count open tags in page $otag = "<{$justTheTag} "; /* Open Tag with space */ @@ -54,11 +52,7 @@ final class TagMatches $open = substr_count($lowercasePage, $otag); // Count open tags in page } - if (substr($tagName, $slen - 1, 1) === '?') { - $ctag = ""; // Close Tag - } + $ctag = ""; // Close Tag $closed = substr_count($lowercasePage, $ctag); // Count Close tags in page $totalStillOpen = $open - $closed; // Difference of open vs. closed....