Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
262 changes: 93 additions & 169 deletions src/ErrorLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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";
Expand All @@ -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
Expand All @@ -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 . '"',
Expand All @@ -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 = '*****';
Expand Down