diff --git a/composer.json b/composer.json index 2283958..17e1686 100644 --- a/composer.json +++ b/composer.json @@ -6,13 +6,14 @@ "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], "authors": [ { - "name": "ADT", - "homepage": "http://appsdevteam.com" + "name": "Apps Dev Team", + "homepage": "https://www.appsdevteam.com" } ], "require": { - "php": ">=7.1", - "tracy/tracy": ">=2.6.0" + "php": ">=7.4", + "tracy/tracy": ">=2.6.0", + "nette/di": "^2.4|^3.0" }, "suggest": { "adt/tracy-git": "Useful for displaying information about currently deployed application version." diff --git a/src/ErrorLogger.php b/src/ErrorLogger.php index 1d2ca2a..1d6640c 100644 --- a/src/ErrorLogger.php +++ b/src/ErrorLogger.php @@ -2,68 +2,54 @@ namespace ADT; +use DateTime; +use Exception; +use Nette\DI\Container; +use RuntimeException; use Tracy\Debugger; use Tracy\Dumper; use Tracy\Helpers; +use Tracy\Logger; -class ErrorLogger extends \Tracy\Logger +class ErrorLogger extends Logger { - /** - * Cesta k souboru s deníkem chyb - * @var string - */ - protected $logFile; - /** * Maximální počet odeslaných emailů denně * @var int */ - protected $maxEmailsPerDay; + protected int $maxEmailsPerDay; /** * Maximální počet odeslaných emailů v rámci jednoho requestu * @var int */ - protected $maxEmailsPerRequest; + protected int $maxEmailsPerRequest; /** * Počet odeslaných emailů v rámci aktuálního requestu * @var int */ - protected $sentEmailsPerRequest = 0; + protected int $sentEmailsPerRequest = 0; /** * Pole s citlivými údaji, jejižch hodnoty se nemají zobrazovat ve výpisu. * @var array */ - protected $sensitiveFields = []; + protected array $sensitiveFields = []; /** * Ma se vlozit error message do emailu - * @var int */ - protected $includeErrorMessage = true; + protected bool $includeErrorMessage = true; - /** - * @var \Nette\DI\Container - */ - protected $container; + protected ?Container $container = null; - /** - * Statická instalace v bootstrap.php - * @param $email - * @param null $maxEmailPerDay - * @param null $maxEmailsPerRequest - * @param array $sensitiveFields TODO: V příští verzi poslední 3 parametry předávat jako pole $options. - * @return ErrorLogger|void - */ - public static function install($email, $maxEmailsPerDay = NULL, $maxEmailsPerRequest = NULL, $sensitiveFields = [], $includeErrorMessage = true) + public static function install($email, $maxEmailsPerDay = NULL, $maxEmailsPerRequest = NULL, $sensitiveFields = [], $includeErrorMessage = true): ?Logger { if (!Debugger::$productionMode) { - return; + return null; } - Debugger::$maxLen = FALSE; Debugger::$email = $email; $logger = new static(Debugger::$logDirectory, Debugger::$email, Debugger::getBlueScreen()); @@ -78,175 +64,114 @@ public static function install($email, $maxEmailsPerDay = NULL, $maxEmailsPerReq return $logger; } - public function __construct($directory, $email = NULL, \Tracy\BlueScreen $blueScreen = NULL) - { - parent::__construct($directory, $email, $blueScreen); - - $this->logFile = $this->directory . '/email-sent'; - $this->fromEmail = $email; - } - - public function setup(\Nette\DI\Container $container) + public function setup(Container $container) { $this->container = $container; } - /** - * Logs message or exception to file and sends email notification. - * @param string|\Exception - * @param int one of constant ILogger::INFO, WARNING, ERROR (sends email), EXCEPTION (sends email), CRITICAL (sends email) - * @return string logged error filename - */ - public function log($message, $priority = self::INFO) + protected function sendEmail($message): void { - if (!$this->directory) { - throw new \LogicException('Directory is not specified.'); - } elseif (!is_dir($this->directory)) { - throw new \RuntimeException("Directory '$this->directory' is not found or is not directory."); - } - - $exceptionFile = $message instanceof \Throwable ? $this->logException($message) : NULL; - $line = $this->formatLogLine($message, $exceptionFile); - $file = $this->directory . '/' . strtolower($priority ?: self::INFO) . '.log'; + if ( + $this->email + && + $this->mailer + ) { + $exceptionFile = $this->getExceptionFile($message); + if (!file_exists($exceptionFile)) { + $exceptionFile = null; + } + $line = static::formatLogLine($message, $exceptionFile); + $logFile = $this->directory . '/email-sent'; - if (!@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX)) { - throw new \RuntimeException("Unable to write to log file '$file'. Is directory writable?"); - } + // we delete email-sent file from yesterday + if (date('Y-m-d', @filemtime($logFile)) < (new DateTime('midnight'))->format('Y-m-d')) { + @unlink($logFile); + } - if (in_array($priority, array(self::ERROR, self::EXCEPTION, self::CRITICAL), TRUE)) { - if ($this->email && $this->mailer) { - $messageHash = md5(preg_replace('~(Resource id #)\d+~', '$1', $message)); - $logContents = @file_get_contents($this->logFile, LOCK_SH); - $today = (new \DateTime)->format('Y-m-d'); - $saveLog = FALSE; - - $log = json_decode($logContents, TRUE); - if (json_last_error() && !empty($logContents)) { - // pokud se nepovede parsování JSONu, zřejmě je log ještě ve starém formátu - $log = [ - 'hashes' => explode(PHP_EOL, $logContents), - 'counter' => 0, - 'date' => $today, - ]; - $saveLog = TRUE; - } else if (empty($logContents)) { - // prázdný nebo neexistující soubor - $log = [ - 'hashes' => [], - 'counter' => 0, - 'date' => $today, - ]; - $saveLog = TRUE; + $messageHash = md5(preg_replace('~(Resource id #)\d+~', '$1', $message)); + + if ( + // ještě se vejdeme do limitu v rámci aktuálního requestu + $this->sentEmailsPerRequest < $this->maxEmailsPerRequest + && + // tento hash jsme ještě neposlali + ( + !($logContent = @file_get_contents($logFile)) + || + (strstr($logContent, $messageHash) === false) + ) + && + // ještě se vejdeme do limitu v rámci aktuálního dne + substr_count($logContent, date('Y-m-d')) < $this->maxEmailsPerDay + ) { + if (!@file_put_contents($logFile, $line . ' ' . $messageHash . PHP_EOL, FILE_APPEND | LOCK_EX)) { + throw new RuntimeException("Unable to write to log file '" . $logFile . "'. Is directory writable?"); } - $sendEmail = ( - // ještě se vejdeme do limitu v rámci aktuálního requestu - $this->sentEmailsPerRequest < $this->maxEmailsPerRequest - && - // tento hash jsme ještě neposlali - !in_array($messageHash, $log['hashes'], TRUE) - && ( - // dnes je to první email - $log['date'] !== $today - || - // ještě se vejdeme do limitu - $log['counter'] < $this->maxEmailsPerDay - ) - ); - - if ($log['date'] !== $today) { - // změnilo se datum, resetujeme počítadlo a datum aktualizujeme - $log['date'] = $today; - $log['counter'] = 0; - $saveLog = TRUE; + // sestavíme zprávu + if (is_array($message)) { + $stringMessage = implode(' ', $message); + } else { + $stringMessage = $message; } - if ($sendEmail) { - // zalogujeme hash a inkrementujeme počítadlo - $log['hashes'][] = $messageHash; - $log['counter']++; - $saveLog = TRUE; - } + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + if (count($backtrace) > 3) { //pokud jsou 3 tak jde pouze o exception a je ulozena nette chybova stranka + $backtraceString = ""; - if ($saveLog) { - $logContents = json_encode($log); - @file_put_contents($this->logFile, $logContents, LOCK_EX); - } + for ($i = 0; $i < count($backtrace); $i++) { + $backtraceData = $backtrace[$i] + [ + 'file' => '_unknown_', + 'line' => '_unknown_', + 'function' => '_unknown_', + ]; - if ($sendEmail) { - // sestavíme zprávu - if (is_array($message)) { - $stringMessage = implode(' ', $message); - } else { - $stringMessage = $message; + $backtraceString = "#$i {$backtraceData['file']}({$backtraceData['line']}): " + . (isset($backtraceData['class']) ? $backtraceData['class'] . '::' : '') + . "{$backtraceData['function']}()\n"; } - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - if (count($backtrace) > 3) { //pokud jsou 3 tak jde pouze o exception a je ulozena nette chybova stranka - $backtraceString = ""; - - for ($i = 0; $i < count($backtrace); $i++) { - $backtraceData = $backtrace[$i] + [ - 'file' => '_unknown_', - 'line' => '_unknown_', - 'function' => '_unknown_', - ]; - - $backtraceString = "#$i {$backtraceData['file']}({$backtraceData['line']}): " - . (isset($backtraceData['class']) ? $backtraceData['class'] . '::' : '') - . "{$backtraceData['function']}()\n"; - } + $stringMessage .= "\n\n" . $backtraceString; + } - $stringMessage .= "\n\n" . $backtraceString; - } + // přidáme doplnující info - referer, browser... + $stringMessage .= "\n\n" . + (isset($_SERVER['HTTP_HOST']) ? 'LINK:' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . "\n" : '') . + 'SERVER:' . Dumper::toText($_SERVER) . "\n\n" . + 'GET:' . Dumper::toText($_GET, [Dumper::DEPTH => 10]) . "\n\n" . + 'POST:' . Dumper::toText($this->hideSensitiveFieldValue($_POST), [Dumper::DEPTH => 10]); - // přidáme doplnující info - referer, browser... - $stringMessage .= "\n\n" . - (isset($_SERVER['HTTP_HOST']) ? 'LINK:' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . "\n" : '') . - 'SERVER:' . Dumper::toText($_SERVER) . "\n\n" . - 'GET:' . Dumper::toText($_GET, [Dumper::DEPTH => 10]) . "\n\n" . - 'POST:' . Dumper::toText($this->hideSensitiveFieldValue($_POST), [Dumper::DEPTH => 10]); - - if ($this->container && ($securityUser = $this->container->getByType('\Nette\Security\User', FALSE))) { - // obalujeme do try protoze SecurityUser je zavisly na databazi a pokud je chyba v db, tak nam error nedojde - try { - $stringMessage .= "\n\n" . - 'securityUser:' . Dumper::toText($securityUser->identity, [Dumper::DEPTH => 1]); - } catch (\Exception $e) { - } - } + if ($this->container && ($securityUser = $this->container->getByType('\Nette\Security\User', FALSE))) { + // obalujeme do try protoze SecurityUser je zavisly na databazi a pokud je chyba v db, tak nam error nedojde + try { + $stringMessage .= "\n\n" . + 'securityUser:' . Dumper::toText($securityUser->identity, [Dumper::DEPTH => 1]); + } catch (Exception $e) {} + } - if ($this->container && ($git = $this->container->getByType('\ADT\TracyGit\Git', FALSE)) !== NULL && ($gitInfo = $git->getInfo())) { - $stringMessage .= "\n\n"; + if ($this->container && ($git = $this->container->getByType('\ADT\TracyGit\Git', FALSE)) !== NULL && ($gitInfo = $git->getInfo())) { + $stringMessage .= "\n\n"; - foreach ($git->getInfo() as $key => $value) { - $stringMessage .= $key . ": " . $value . "\n"; - } + foreach ($gitInfo as $key => $value) { + $stringMessage .= $key . ": " . $value . "\n"; } + } - // odešleme chybu emailem - call_user_func($this->mailer, $stringMessage, implode(', ', (array)$this->email), $exceptionFile); + // odešleme chybu emailem + call_user_func($this->mailer, $stringMessage, implode(', ', (array)$this->email), $exceptionFile); - $this->sentEmailsPerRequest++; - } + $this->sentEmailsPerRequest++; } } - - return $exceptionFile; } - /** - * Default mailer. - * @param string|\Exception|\Throwable - * @param string - * @return void * @internal */ - public function defaultMailer($message, string $email, $attachment = NULL): void + public function defaultMailer($message, string $email, ?string $attachment = null): void { - $host = preg_replace('#[^\w.-]+#', '', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n')); + $host = preg_replace('#[^\w.-]+#', '', $_SERVER['HTTP_HOST'] ?? php_uname('n')); $separator = md5(time()); $eol = "\n"; @@ -261,7 +186,7 @@ public function defaultMailer($message, string $email, $attachment = NULL): void "Content-Transfer-Encoding: 8bit" . $eol . $eol . $this->formatMessage($message) . "\n\nsource: " . Helpers::getSource() . $eol . "--" . $separator . $eol; - + if ($attachment) { $body .= // Attachment @@ -278,7 +203,6 @@ public function defaultMailer($message, string $email, $attachment = NULL): void ["\n", PHP_EOL], [ 'headers' => implode("\n", [ - 'From: ' . ($this->fromEmail ?: "noreply@$host"), 'X-Mailer: Tracy', 'MIME-Version: 1.0', 'Content-Type: multipart/mixed; boundary="' . $separator . '"', @@ -292,7 +216,7 @@ public function defaultMailer($message, string $email, $attachment = NULL): void mail($email, $parts['subject'], $parts['body'], $parts['headers']); } - protected function hideSensitiveFieldValue($array) + protected function hideSensitiveFieldValue($array): array { $sensitiveFields = $this->sensitiveFields; $replacement = '*****';