commit
5ef6744f32
@ -0,0 +1,122 @@ |
||||
<?php |
||||
|
||||
declare(strict_types=1); |
||||
|
||||
namespace Chamilo\CoreBundle\ServiceHelper; |
||||
|
||||
use Symfony\Component\HttpKernel\KernelInterface; |
||||
use Symfony\Contracts\Translation\TranslatorInterface; |
||||
use DateTime; |
||||
use SplFileObject; |
||||
|
||||
class LoginAttemptLogger |
||||
{ |
||||
private string $logDir; |
||||
private TranslatorInterface $translator; |
||||
|
||||
private int $maxLogs; |
||||
|
||||
public function __construct(KernelInterface $kernel, TranslatorInterface $translator) |
||||
{ |
||||
$this->logDir = $kernel->getLogDir() . '/ids'; |
||||
$this->translator = $translator; |
||||
$this->maxLogs = 90; // for how many iterations to keep log files |
||||
|
||||
if (!is_dir($this->logDir)) { |
||||
mkdir($this->logDir, 0777, true); |
||||
} |
||||
} |
||||
|
||||
public function logAttempt(bool $status, string $username, string $ip): void |
||||
{ |
||||
$logDir = $this->logDir; |
||||
if (!is_dir($logDir)) { |
||||
mkdir($logDir, 0777, true); |
||||
} |
||||
|
||||
$date = new DateTime(); |
||||
$logFilePrefix = $logDir . '/ids'; |
||||
$logFile = $logFilePrefix . '.log'; |
||||
|
||||
// Most of the time, there will be a previous log file |
||||
if (file_exists($logFile)) { |
||||
// Read the last line of the existing log file to determine if we need rotation |
||||
$lastLine = $this->_readLastLine($logFile); |
||||
if (!$this->_checkDateIsToday($lastLine)) { |
||||
// The last line's date is not today, so we need to rotate |
||||
$this->_rotateLogFiles($logFile, $this->maxLogs); |
||||
} |
||||
} |
||||
|
||||
$statusText = $this->translator->trans($status ? 'succeeded' : 'failed'); |
||||
$infoText = $this->translator->trans('info'); |
||||
$clientText = $this->translator->trans('client'); |
||||
$loginMessage = $this->translator->trans('Login %status% for username %username%', ['%status%' => $statusText, '%username%' => $username]); |
||||
|
||||
$logMessage = sprintf("[%s] [%s] [%s %s] %s\n", |
||||
$date->format('Y-m-d H:i:s'), |
||||
$infoText, |
||||
$clientText, |
||||
$ip, |
||||
$loginMessage |
||||
); |
||||
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX); |
||||
} |
||||
|
||||
/** |
||||
* Efficiently read the last line of the provided file, or an empty string |
||||
* @param string $logFilePath |
||||
* @return string |
||||
*/ |
||||
private function _readLastLine(string $logFilePath): string |
||||
{ |
||||
$fileObject = new SplFileObject($logFilePath, 'r'); |
||||
$fileObject->seek(PHP_INT_MAX); |
||||
$fileObject->seek($fileObject->key() - 1); |
||||
$line = $fileObject->current(); |
||||
if (empty($line)) { |
||||
return ''; |
||||
} |
||||
|
||||
return $line; |
||||
} |
||||
/** |
||||
* Check if the date in a log line is same as today |
||||
* @param string $line A line of type "[2024-03-01 09:44:57] [info] [client 127.0.0.1] Some text" |
||||
*/ |
||||
private function _checkDateIsToday(string $line): bool |
||||
{ |
||||
$date = new DateTime(); |
||||
$today = $date->format('Y-m-d'); |
||||
$matches = []; |
||||
if (!empty($line) && preg_match('/\[(\d{4}-\d{2}-\d{2})\s.*/', $line, $matches)) { |
||||
if (0 === strcmp($matches[1], $today)) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Rotate log files |
||||
* @param string $baseLogFile |
||||
* @param int $maxLogs |
||||
* @return void |
||||
*/ |
||||
private function _rotateLogFiles(string $baseLogFile, int $maxLogs): void |
||||
{ |
||||
for ($i = $maxLogs; $i > 0; $i--) { |
||||
$oldLog = $baseLogFile.'.'.$i; |
||||
$newLog = $baseLogFile.'.'.($i + 1); |
||||
if (file_exists($oldLog)) { |
||||
if (file_exists($newLog)) { |
||||
unlink($newLog); |
||||
} |
||||
rename($oldLog, $newLog); |
||||
} |
||||
} |
||||
rename($baseLogFile, $baseLogFile.'.1'); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue