LogRepository   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 11
eloc 57
dl 0
loc 118
rs 10
c 6
b 0
f 0
ccs 0
cts 63
cp 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A failedOften() 0 32 5
A registerOften() 0 3 1
A loginFailedOften() 0 3 1
A log() 0 15 1
A updatePasswordFailedOften() 0 3 1
A requestPasswordResetOften() 0 3 1
A deleteOldLogs() 0 23 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Repository\Traits;
6
7
use Cake\Chronos\Chronos;
8
use Doctrine\DBAL\ArrayParameterType;
9
use Doctrine\ORM\EntityManager;
10
use Doctrine\ORM\QueryBuilder;
11
use Ecodev\Felix\Repository\LogRepository as LogRepositoryInterface;
12
use Monolog\Level;
13
use Monolog\LogRecord;
14
15
trait LogRepository
16
{
17
    /**
18
     * @return EntityManager
19
     */
20
    abstract protected function getEntityManager();
21
22
    /**
23
     * Creates a new QueryBuilder instance that is prepopulated for this entity name.
24
     */
25
    abstract public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder;
26
27
    /**
28
     * This should NOT be called directly, instead use `_log()` to log stuff.
29
     */
30
    public function log(LogRecord $record): void
31
    {
32
        $data = [
33
            'level' => $record->level->value,
34
            'message' => $record->message,
35
            'creation_date' => Chronos::instance($record->datetime)->toDateTimeString(),
36
            'creator_id' => $record->extra['creator_id'] ?? null,
37
            'url' => $record->extra['url'] ?? '',
38
            'referer' => $record->extra['referer'] ?? '',
39
            'request' => $record->extra['request'] ?? '',
40
            'ip' => $record->extra['ip'] ?? '',
41
            'context' => json_encode($record->context, JSON_THROW_ON_ERROR),
42
        ];
43
44
        $this->getEntityManager()->getConnection()->insert('log', $data);
45
    }
46
47
    /**
48
     * Returns whether the current IP often failed to log in.
49
     */
50
    public function loginFailedOften(): bool
51
    {
52
        return $this->failedOften(LogRepositoryInterface::LOGIN, LogRepositoryInterface::LOGIN_FAILED);
53
    }
54
55
    public function updatePasswordFailedOften(): bool
56
    {
57
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::UPDATE_PASSWORD_FAILED);
58
    }
59
60
    public function requestPasswordResetOften(): bool
61
    {
62
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::REQUEST_PASSWORD_RESET, 10);
63
    }
64
65
    public function registerOften(): bool
66
    {
67
        return $this->failedOften(LogRepositoryInterface::REGISTER_CONFIRM, LogRepositoryInterface::REGISTER, 10);
68
    }
69
70
    private function failedOften(string $success, string $failed, int $maxFailureCount = 20): bool
71
    {
72
        if (PHP_SAPI === 'cli') {
73
            $ip = !empty(getenv('REMOTE_ADDR')) ? getenv('REMOTE_ADDR') : 'script';
74
        } else {
75
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
76
        }
77
78
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
79
            ->select('message')
80
            ->from('log')
81
            ->andWhere('level = :level')
82
            ->setParameter('level', Level::Info->value)
83
            ->andWhere('message IN (:message)')
84
            ->setParameter('message', [$success, $failed], ArrayParameterType::STRING)
85
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
86
            ->andWhere('ip = :ip')
87
            ->setParameter('ip', $ip)
88
            ->orderBy('id', 'DESC');
89
90
        $events = $select->executeQuery()->fetchFirstColumn();
91
92
        // Goes from present to past and count failure, until the last time we succeeded logging in
93
        $failureCount = 0;
94
        foreach ($events as $event) {
95
            if ($event === $success) {
96
                break;
97
            }
98
            ++$failureCount;
99
        }
100
101
        return $failureCount > $maxFailureCount;
102
    }
103
104
    /**
105
     * Delete log entries which are errors/warnings and older than two months
106
     * We always keep LogLevel::INFO level because we use it for statistics.
107
     *
108
     * @return int the count deleted logs
109
     */
110
    public function deleteOldLogs(): int
111
    {
112
        $connection = $this->getEntityManager()->getConnection();
113
        $query = $connection->createQueryBuilder()
114
            ->delete('log')
115
            ->andWhere('log.level != :level OR message IN (:message)')
116
            ->setParameter('level', Level::Info->value)
117
            ->setParameter('message', [
118
                LogRepositoryInterface::LOGIN,
119
                LogRepositoryInterface::LOGIN_FAILED,
120
                LogRepositoryInterface::UPDATE_PASSWORD,
121
                LogRepositoryInterface::REQUEST_PASSWORD_RESET,
122
                LogRepositoryInterface::UPDATE_PASSWORD_FAILED,
123
                LogRepositoryInterface::REGISTER,
124
                LogRepositoryInterface::REGISTER_CONFIRM,
125
            ], ArrayParameterType::STRING)
126
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 2 MONTH)');
127
128
        $connection->executeStatement('LOCK TABLES `log` WRITE;');
129
        $count = $query->executeStatement();
130
        $connection->executeStatement('UNLOCK TABLES;');
131
132
        return $count;
133
    }
134
}
135