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 = "{$justTheTag}"; // Close Tag IE h1,h2
- } else {
- $ctag = "{$justTheTag}>"; // Close Tag
- }
+ $ctag = "{$justTheTag}>"; // Close Tag
$closed = substr_count($lowercasePage, $ctag); // Count Close tags in page
$totalStillOpen = $open - $closed; // Difference of open vs. closed....