Failed Conditions
Push — master ( 5e6761...398dce )
by Adrien
04:14 queued 01:57
created

LogRepository   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 100
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 48
c 1
b 0
f 0
dl 0
loc 100
rs 10
ccs 0
cts 67
cp 0
wmc 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A failedOften() 0 32 4
A loginFailedOften() 0 3 1
A log() 0 6 1
A getLoginDate() 0 13 2
A updatePasswordFailedOften() 0 3 1
A deleteOldLogs() 0 15 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\Connection;
9
use Ecodev\Felix\Model\User;
10
use Ecodev\Felix\Repository\LogRepository as LogRepositoryInterface;
11
use Laminas\Log\Logger;
12
13
trait LogRepository
14
{
15
    /**
16
     * This should NOT be called directly, instead use `_log()` to log stuff
17
     *
18
     * @param array $event
19
     */
20
    public function log(array $event): void
21
    {
22
        $event['creation_date'] = Chronos::instance($event['creation_date'])->toIso8601String();
23
        $event['extra'] = json_encode($event['extra']);
24
25
        $this->getEntityManager()->getConnection()->insert('log', $event);
0 ignored issues
show
Bug introduced by
It seems like getEntityManager() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

25
        $this->/** @scrutinizer ignore-call */ 
26
               getEntityManager()->getConnection()->insert('log', $event);
Loading history...
26
    }
27
28
    /**
29
     * Returns whether the current IP often failed to login
30
     *
31
     * @return bool
32
     */
33
    public function loginFailedOften(): bool
34
    {
35
        return $this->failedOften(LogRepositoryInterface::LOGIN, LogRepositoryInterface::LOGIN_FAILED);
36
    }
37
38
    public function updatePasswordFailedOften(): bool
39
    {
40
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::UPDATE_PASSWORD_FAILED);
41
    }
42
43
    private function failedOften(string $success, string $failed): bool
44
    {
45
        if (PHP_SAPI === 'cli') {
46
            $ip = 'script';
47
        } else {
48
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
49
        }
50
51
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
52
            ->select('message')
53
            ->from('log')
54
            ->andWhere('priority = :priority')
55
            ->setParameter('priority', Logger::INFO)
56
            ->andWhere('message IN (:message)')
57
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
58
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
59
            ->andWhere('ip = :ip')
60
            ->setParameter('ip', $ip)
61
            ->orderBy('id', 'DESC');
62
63
        $events = $select->execute()->fetchAll(\PDO::FETCH_COLUMN);
64
65
        // Goes from present to past and count failure, until the last time we succeeded logging in
66
        $failureCount = 0;
67
        foreach ($events as $event) {
68
            if ($event === $success) {
69
                break;
70
            }
71
            ++$failureCount;
72
        }
73
74
        return $failureCount > 20;
75
    }
76
77
    /**
78
     * Delete log entries which are errors/warnings and older than one month
79
     * We always keep Logger::INFO level because we use it for statistics
80
     *
81
     * @return int the count deleted logs
82
     */
83
    public function deleteOldLogs(): int
84
    {
85
        $connection = $this->getEntityManager()->getConnection();
86
        $query = $connection->createQueryBuilder()
87
            ->delete('log')
88
            ->andWhere('log.priority != :priority OR message = :message')
89
            ->setParameter('priority', Logger::INFO)
90
            ->setParameter('message', LogRepositoryInterface::LOGIN_FAILED)
91
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
92
93
        $connection->query('LOCK TABLES `log` WRITE;');
94
        $count = $query->execute();
95
        $connection->query('UNLOCK TABLES;');
96
97
        return $count;
98
    }
99
100
    public function getLoginDate(User $user, bool $first): ?Chronos
101
    {
102
        $qb = $this->createQueryBuilder('log')
0 ignored issues
show
Bug introduced by
It seems like createQueryBuilder() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

102
        $qb = $this->/** @scrutinizer ignore-call */ createQueryBuilder('log')
Loading history...
103
            ->select('log.creationDate')
104
            ->andWhere('log.creator = :user')
105
            ->andWhere('log.message = :message')
106
            ->setParameter('user', $user)
107
            ->setParameter('message', LogRepositoryInterface::LOGIN)
108
            ->addOrderBy('log.creationDate', $first ? 'ASC' : 'DESC');
109
110
        $result = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
111
112
        return $result['creationDate'] ?? null;
113
    }
114
}
115