Error Logging

main
Robert 2 days ago
parent b1715e8f3e
commit dd22f11bdb
  1. 12
      src/Framework/Common.php
  2. 34
      src/Framework/ErrorHandler.php
  3. 90
      src/Framework/Logger.php
  4. 8
      src/Framework/Middleware/ErrorMiddleware.php
  5. 16
      src/Framework/Security.php
  6. 12
      src/Framework/TagMatches.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;

@ -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 = "<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
{
$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;

@ -11,8 +11,11 @@ use Psr\Log\InvalidArgumentException;
final class Logger implements LoggerInterface
{
private $handle = false;
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 = [
@ -25,15 +28,11 @@ final class Logger implements LoggerInterface
LogLevel::INFO => 200,
LogLevel::DEBUG => 100,
];
private const ENV_LEVEL_MAP = [
'production' => LogLevel::ERROR,
'prod' => LogLevel::ERROR,
'staging' => LogLevel::WARNING,
'testing' => LogLevel::NOTICE,
'development' => LogLevel::DEBUG,
'dev' => LogLevel::DEBUG,
'local' => LogLevel::DEBUG,
@ -43,7 +42,8 @@ final class Logger implements LoggerInterface
string $filename = 'system',
int $maxCount = 1000,
?string $minLevel = null
) {
)
{
$env = self::detectEnv();
if ($minLevel === null) {
@ -65,7 +65,7 @@ final class Logger implements LoggerInterface
return false;
}
$logDir = Requires::filterDirPath(BaseDir . '/protected/logs');
$logDir = Requires::filterDirPath(BaseDir . self::LOGS_DIR);
if (!Requires::isValidFile($logDir)) {
return false;
}
@ -81,10 +81,10 @@ final class Logger implements LoggerInterface
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)) {
@ -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;
}
@ -124,10 +170,17 @@ final class Logger implements LoggerInterface
$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)
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,11 +232,11 @@ 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')
);
}

@ -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]
);

@ -328,6 +328,19 @@ class Security
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!!
* Checks for PHP tags inside of file.
@ -339,8 +352,7 @@ class Security
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {
$line = fgets($file_handle);
$pos = strpos($line, '<?php');
if ($pos !== false) {
if (self::doesStringContainsPhp($line)) {
fclose($file_handle);
return true;
}

@ -16,7 +16,7 @@ final class TagMatches
{
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>';
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
}
$closed = substr_count($lowercasePage, $ctag); // Count Close tags in page
$totalStillOpen = $open - $closed; // Difference of open vs. closed....

Loading…
Cancel
Save