Error Logging

main
Robert 2 days ago
parent b1715e8f3e
commit dd22f11bdb
  1. 12
      src/Framework/Common.php
  2. 34
      src/Framework/ErrorHandler.php
  3. 142
      src/Framework/Logger.php
  4. 8
      src/Framework/Middleware/ErrorMiddleware.php
  5. 16
      src/Framework/Security.php
  6. 14
      src/Framework/TagMatches.php

@ -4,9 +4,12 @@ declare(strict_types=1);
namespace IOcornerstone\Framework; namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Enum\ExitOnDump as endDump; use IOcornerstone\Framework\{
use IOcornerstone\Framework\Console; Console,
use IOcornerstone\Framework\String\StringFacade as F; Logger,
String\StringFacade as F,
Enum\ExitOnDump as endDump,
};
final class Common final class Common
{ {
@ -47,7 +50,8 @@ final class Common
$useLogger = $o->useLogger ?? false; $useLogger = $o->useLogger ?? false;
if ($useLogger) { if ($useLogger) {
\IOcornerstone\doLogger()->alert($combined); $logger = new Logger();
$logger->alert($combined);
return $combined; return $combined;
} }
$doDump = $o->doDump ?? false; $doDump = $o->doDump ?? false;

@ -96,6 +96,8 @@ final class ErrorHandler
$e = new ErrorException('Unknown error'); $e = new ErrorException('Unknown error');
} }
$this->logException($e);
if ($this->isJsonRequest()) { if ($this->isJsonRequest()) {
if ($this->debug) { if ($this->debug) {
$this->renderJsonDebug($e); $this->renderJsonDebug($e);
@ -110,7 +112,6 @@ final class ErrorHandler
$this->renderConsole($e); $this->renderConsole($e);
} else { } else {
$this->renderProductionConsole($e); $this->renderProductionConsole($e);
$this->logException($e);
} }
return true; return true;
} }
@ -123,7 +124,6 @@ final class ErrorHandler
$this->renderDebug($e); $this->renderDebug($e);
} else { } else {
$this->renderProduction($e); $this->renderProduction($e);
$this->logException($e);
} }
// Don't execute PHP's internal error handler // Don't execute PHP's internal error handler
return true; return true;
@ -163,7 +163,7 @@ final class ErrorHandler
], true); ], true);
} }
private function getErrorType(Throwable $e): string public function getErrorType(Throwable $e): string
{ {
if ($e instanceof ErrorException) { if ($e instanceof ErrorException) {
return match ($e->getSeverity()) { return match ($e->getSeverity()) {
@ -313,6 +313,33 @@ final class ErrorHandler
echo $color . $out . PHP_EOL; 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 = "<div class=\"$style\">" . 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 .= "</div>" . PHP_EOL;
$ret .= "<hr />" . PHP_EOL;
return $ret;
}
public function getCodeNumber(Throwable $e): string public function getCodeNumber(Throwable $e): string
{ {
$dCode = $e->getCode() ?? 0; $dCode = $e->getCode() ?? 0;
@ -387,6 +414,7 @@ final class ErrorHandler
private function setLoggerByLevel(Throwable $e): void private function setLoggerByLevel(Throwable $e): void
{ {
$errorMessage = (string) $e; $errorMessage = (string) $e;
$logger = new Logger(); $logger = new Logger();
if ($logger === false) { if ($logger === false) {
return; return;

@ -11,39 +11,39 @@ use Psr\Log\InvalidArgumentException;
final class Logger implements LoggerInterface 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 $handle = false;
private string $minLevel; private string $minLevel;
private const LEVEL_PRIORITY = [ private const LEVEL_PRIORITY = [
LogLevel::EMERGENCY => 600, LogLevel::EMERGENCY => 600,
LogLevel::ALERT => 550, LogLevel::ALERT => 550,
LogLevel::CRITICAL => 500, LogLevel::CRITICAL => 500,
LogLevel::ERROR => 400, LogLevel::ERROR => 400,
LogLevel::WARNING => 300, LogLevel::WARNING => 300,
LogLevel::NOTICE => 250, LogLevel::NOTICE => 250,
LogLevel::INFO => 200, LogLevel::INFO => 200,
LogLevel::DEBUG => 100, LogLevel::DEBUG => 100,
]; ];
private const ENV_LEVEL_MAP = [ private const ENV_LEVEL_MAP = [
'production' => LogLevel::ERROR, 'production' => LogLevel::ERROR,
'prod' => LogLevel::ERROR, 'prod' => LogLevel::ERROR,
'staging' => LogLevel::WARNING,
'staging' => LogLevel::WARNING, 'testing' => LogLevel::NOTICE,
'testing' => LogLevel::NOTICE,
'development' => LogLevel::DEBUG, 'development' => LogLevel::DEBUG,
'dev' => LogLevel::DEBUG, 'dev' => LogLevel::DEBUG,
'local' => LogLevel::DEBUG, 'local' => LogLevel::DEBUG,
]; ];
public function __construct( public function __construct(
string $filename = 'system', string $filename = 'system',
int $maxCount = 1000, int $maxCount = 1000,
?string $minLevel = null ?string $minLevel = null
) { )
{
$env = self::detectEnv(); $env = self::detectEnv();
if ($minLevel === null) { if ($minLevel === null) {
@ -53,7 +53,7 @@ final class Logger implements LoggerInterface
if (!isset(self::LEVEL_PRIORITY[$minLevel])) { if (!isset(self::LEVEL_PRIORITY[$minLevel])) {
throw new InvalidArgumentException('Invalid log level: ' . $minLevel); throw new InvalidArgumentException('Invalid log level: ' . $minLevel);
} }
$this->minLevel = $minLevel; $this->minLevel = $minLevel;
// Stop Up Level attacks // Stop Up Level attacks
@ -61,12 +61,12 @@ final class Logger implements LoggerInterface
return false; return false;
} }
if (! defined("BaseDir")) { if (!defined("BaseDir")) {
return false; return false;
} }
$logDir = Requires::filterDirPath(BaseDir . '/protected/logs'); $logDir = Requires::filterDirPath(BaseDir . self::LOGS_DIR);
if (! Requires::isValidFile($logDir)) { if (!Requires::isValidFile($logDir)) {
return false; return false;
} }
@ -74,20 +74,20 @@ final class Logger implements LoggerInterface
mkdir($logDir, 0775, true); mkdir($logDir, 0775, true);
} }
if (! Requires::isValidFile($filename)) { if (!Requires::isValidFile($filename)) {
return false; return false;
} }
$safeFileName = Requires::filterFileName($filename); $safeFileName = Requires::filterFileName($filename);
if ($safeFileName === false || $safeFileName === "") { if ($safeFileName === false || $safeFileName === "") {
return false; return false;
} }
$file = $logDir . '/' . $safeFileName . '.log.txt'; $file = $logDir . '/' . $safeFileName . self::LOGS_EXT;
if ($maxCount > 1 && $this->getLines($file) > $maxCount) { if ($maxCount > 5 && $this->getLines($file) > $maxCount) {
@unlink($file); $aLogs = $this->rotateLog($file, $safeFileName);
} }
if (! file_exists($file)) { if (!file_exists($file)) {
if (file_put_contents($file, "\n", FILE_APPEND) === false) { if (file_put_contents($file, "\n", FILE_APPEND) === false) {
return false; return false;
} }
@ -100,20 +100,66 @@ final class Logger implements LoggerInterface
} }
$this->handle = fopen($file, 'ab'); $this->handle = fopen($file, 'ab');
if (isset($aLogs)) {
$this->alert("Purged from Logs = " . $aLogs['logsDeleted']);
}
return true; 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 ---------------- */ /* ---------------- 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])) { if (!is_string($level) || !isset(self::LEVEL_PRIORITY[$level])) {
throw new InvalidArgumentException('Invalid log level'); throw new InvalidArgumentException('Invalid log level');
} }
if ( if (
self::LEVEL_PRIORITY[$level] self::LEVEL_PRIORITY[$level] < self::LEVEL_PRIORITY[$this->minLevel]
< self::LEVEL_PRIORITY[$this->minLevel]
) { ) {
return; return;
} }
@ -122,12 +168,19 @@ final class Logger implements LoggerInterface
return; 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'); $time = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
fwrite( fwrite(
$this->handle, $this->handle,
sprintf("[%s] %s: %s\n", $time, strtoupper($level), $message) sprintf("[%s] %s: %s\n", $time, strtoupper($customeLevel), $message)
); );
} }
@ -148,6 +201,11 @@ final class Logger implements LoggerInterface
$this->log(LogLevel::CRITICAL, $message, $context); $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 public function error($message, array $context = []): void
{ {
$this->log(LogLevel::ERROR, $message, $context); $this->log(LogLevel::ERROR, $message, $context);
@ -174,21 +232,21 @@ final class Logger implements LoggerInterface
} }
/* ---------------- Helpers ---------------- */ /* ---------------- Helpers ---------------- */
private static function detectEnv(): string private static function detectEnv(): string
{ {
return strtolower( return strtolower(
getenv('APP_ENV') getenv('APP_ENV') ?: ($_ENV['APP_ENV'] ?? 'production')
?: ($_ENV['APP_ENV'] ?? 'production')
); );
} }
private function interpolate(string $message, array $context): string private function interpolate(string $message, array $context): string
{ {
$replace = []; $replace = [];
foreach ($context as $key => $value) { foreach ($context as $key => $value) {
if (is_scalar($value) || $value instanceof \Stringable) { if (is_scalar($value) || $value instanceof \Stringable) {
$replace['{' . $key . '}'] = (string)$value; $replace['{' . $key . '}'] = (string) $value;
} }
} }

@ -32,9 +32,13 @@ final class ErrorMiddleware implements MiddlewareInterface
try { try {
return $handler->handle($request); return $handler->handle($request);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$sLevel = Reg::get('error_handler')->getErrorType($e);
$codeNumber = Reg::get('error_handler')->getCodeNumber($e); $codeNumber = Reg::get('error_handler')->getCodeNumber($e);
$message = $codeNumber . $e->getMessage(); $niceErrors = Reg::get('error_handler')->getCodeMessage($e);
$this->logger->error( $message = $codeNumber . $niceErrors;
$this->logger->SomeSystemError(
$sLevel,
$message, $message,
['exception' => $e] ['exception' => $e]
); );

@ -328,6 +328,19 @@ class Security
return $ipaddress; return $ipaddress;
} }
public static function doesStringContainsPhp(string $data): bool
{
$pos = strpos($data, '<?php');
if ($pos !== false) {
return true;
}
$epos = strpos($data, '<?=');
if ($epos !== false) {
return true;
}
return false;
}
/** /**
* Make sure uploads (LIKE Images, etc...) do NOT run PHP code!! * Make sure uploads (LIKE Images, etc...) do NOT run PHP code!!
* Checks for PHP tags inside of file. * Checks for PHP tags inside of file.
@ -339,8 +352,7 @@ class Security
$file_handle = fopen($file, "r"); $file_handle = fopen($file, "r");
while (!feof($file_handle)) { while (!feof($file_handle)) {
$line = fgets($file_handle); $line = fgets($file_handle);
$pos = strpos($line, '<?php'); if (self::doesStringContainsPhp($line)) {
if ($pos !== false) {
fclose($file_handle); fclose($file_handle);
return true; return true;
} }

@ -16,7 +16,7 @@ final class TagMatches
{ {
const TAGS_TO_CHECK = ['div', 'span', 'form', const TAGS_TO_CHECK = ['div', 'span', 'form',
'i*', 'a*', 'h?', 'p*', 'td', 'th*', 'tr' 'i*', 'a*', 'h1', 'h2', 'h3', 'p*', 'td', 'th*', 'tr'
]; ];
/** /**
@ -41,10 +41,8 @@ final class TagMatches
$ui_end = '</b></div>'; $ui_end = '</b></div>';
foreach ($array_of_tags as $tagName) { foreach ($array_of_tags as $tagName) {
$slen = strlen($tagName); $justTheTag = str_replace('*', '', $tagName);
$tagNameWithoutStar = str_replace('*', '', $tagName); if (str_contains($tagName, "*")) {
$justTheTag = str_replace('?', '', $tagNameWithoutStar);
if (substr($tagName, $slen - 1, 1) === '*') {
$otag = "<{$justTheTag}>"; // Open Tag $otag = "<{$justTheTag}>"; // Open Tag
$open = substr_count($lowercasePage, $otag); // Count open tags in page $open = substr_count($lowercasePage, $otag); // Count open tags in page
$otag = "<{$justTheTag} "; /* Open Tag with space */ $otag = "<{$justTheTag} "; /* Open Tag with space */
@ -54,11 +52,7 @@ final class TagMatches
$open = substr_count($lowercasePage, $otag); // Count open tags in page $open = substr_count($lowercasePage, $otag); // Count open tags in page
} }
if (substr($tagName, $slen - 1, 1) === '?') { $ctag = "</{$justTheTag}>"; // Close Tag
$ctag = "</{$justTheTag}"; // Close Tag IE h1,h2
} else {
$ctag = "</{$justTheTag}>"; // Close Tag
}
$closed = substr_count($lowercasePage, $ctag); // Count Close tags in page $closed = substr_count($lowercasePage, $ctag); // Count Close tags in page
$totalStillOpen = $open - $closed; // Difference of open vs. closed.... $totalStillOpen = $open - $closed; // Difference of open vs. closed....

Loading…
Cancel
Save