Failed Conditions
Push — master ( 9f23c0...1448ca )
by Adrien
07:45
created

LogRepository   A

Complexity

Total Complexity 8

Size/Duplication

Total Lines 105
Duplicated Lines 0 %

Test Coverage

Coverage 79.07%

Importance

Changes 0
Metric Value
eloc 43
dl 0
loc 105
ccs 34
cts 43
cp 0.7907
rs 10
c 0
b 0
f 0
wmc 8

5 Methods

Rating   Name   Duplication   Size   Complexity  
A loginFailedOften() 0 3 1
A updatePasswordFailedOften() 0 3 1
A deleteOldLogs() 0 15 1
A log() 0 6 1
A failedOften() 0 32 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Repository;
6
7
use Cake\Chronos\Chronos;
8
use Doctrine\DBAL\Connection;
9
use Zend\Log\Logger;
10
11
class LogRepository extends AbstractRepository
12
{
13
    /**
14
     * Log message to be used when user log in
15
     */
16
    const LOGIN = 'login';
17
18
    /**
19
     * Log message to be used when user cannot log in
20
     */
21
    const LOGIN_FAILED = 'login failed';
22
23
    /**
24
     * Log message to be used when user change his password
25
     */
26
    const UPDATE_PASSWORD = 'update password';
27
28
    /**
29
     * Log message to be used when user cannot change his password
30
     */
31
    const UPDATE_PASSWORD_FAILED = 'update password failed';
32
33
    /**
34
     * This should NOT be called directly, instead use `_log()` to log stuff
35
     *
36
     * @param array $event
37
     */
38
    public function log(array $event): void
39
    {
40
        $event['creation_date'] = Chronos::instance($event['creation_date'])->toIso8601String();
41
        $event['extra'] = json_encode($event['extra']);
42
43
        $this->getEntityManager()->getConnection()->insert('log', $event);
44
    }
45
46
    /**
47
     * Returns whether the current IP often failed to login
48
     *
49
     * @return bool
50
     */
51 1
    public function loginFailedOften(): bool
52
    {
53 1
        return $this->failedOften(self::LOGIN, self::LOGIN_FAILED);
54
    }
55
56 2
    public function updatePasswordFailedOften(): bool
57
    {
58 2
        return $this->failedOften(self::UPDATE_PASSWORD, self::UPDATE_PASSWORD_FAILED);
59
    }
60
61 3
    private function failedOften(string $success, string $failed): bool
62
    {
63 3
        if (PHP_SAPI === 'cli') {
64 3
            $ip = 'script';
65
        } else {
66
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
67
        }
68
69 3
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
70 3
            ->select('message')
71 3
            ->from('log')
72 3
            ->andWhere('priority = :priority')
73 3
            ->setParameter('priority', Logger::INFO)
74 3
            ->andWhere('message IN (:message)')
75 3
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
76 3
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
77 3
            ->andWhere('ip = :ip')
78 3
            ->setParameter('ip', $ip)
79 3
            ->orderBy('id', 'DESC');
80
81 3
        $events = $select->execute()->fetchAll(\PDO::FETCH_COLUMN);
82
83
        // Goes from present to past and count failure, until the last time we succeeded logging in
84 3
        $failureCount = 0;
85 3
        foreach ($events as $event) {
86
            if ($event === $success) {
87
                break;
88
            }
89
            ++$failureCount;
90
        }
91
92 3
        return $failureCount > 5;
93
    }
94
95
    /**
96
     * Delete log entries which are errors/warnings and older than one month
97
     * We always keep Logger::INFO level because we use it for statistics
98
     *
99
     * @return int the count deleted logs
100
     */
101 1
    public function deleteOldLogs(): int
102
    {
103 1
        $connection = $this->getEntityManager()->getConnection();
104 1
        $query = $connection->createQueryBuilder()
105 1
            ->delete('log')
106 1
            ->andWhere('log.priority != :priority OR message = :message')
107 1
            ->setParameter('priority', Logger::INFO)
108 1
            ->setParameter('message', self::LOGIN_FAILED)
109 1
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
110
111 1
        $connection->query('LOCK TABLES `log` WRITE;');
112 1
        $count = $query->execute();
113 1
        $connection->query('UNLOCK TABLES;');
114
115 1
        return $count;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $count could return the type Doctrine\DBAL\Driver\Statement which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
116
    }
117
}
118