Failed Conditions
Push — master ( eb7751...8066fb )
by Adrien
07:27
created

LogRepository::getLoginDate()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 1
nop 2
dl 0
loc 13
ccs 10
cts 10
cp 1
crap 2
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Repository;
6
7
use Application\Model\User;
8
use Cake\Chronos\Chronos;
9
use Doctrine\DBAL\Connection;
10
use Zend\Log\Logger;
11
12
class LogRepository extends AbstractRepository
13
{
14
    /**
15
     * Log message to be used when user log in
16
     */
17
    const LOGIN = 'login';
18
19
    /**
20
     * Log message to be used when user cannot log in
21
     */
22
    const LOGIN_FAILED = 'login failed';
23
24
    /**
25
     * Log message to be used when user change his password
26
     */
27
    const UPDATE_PASSWORD = 'update password';
28
29
    /**
30
     * Log message to be used when user cannot change his password
31
     */
32
    const UPDATE_PASSWORD_FAILED = 'update password failed';
33
34
    /**
35
     * Log message to be used when trying to send email but it's already running
36
     */
37
    const MAILER_LOCKED = 'Unable to obtain lock for mailer, try again later.';
38
39
    /**
40
     * This should NOT be called directly, instead use `_log()` to log stuff
41
     *
42
     * @param array $event
43
     */
44 5
    public function log(array $event): void
45
    {
46 5
        $event['creation_date'] = Chronos::instance($event['creation_date'])->toIso8601String();
47 5
        $event['extra'] = json_encode($event['extra']);
48
49 5
        $this->getEntityManager()->getConnection()->insert('log', $event);
50 5
    }
51
52
    /**
53
     * Returns whether the current IP often failed to login
54
     *
55
     * @return bool
56
     */
57 2
    public function loginFailedOften(): bool
58
    {
59 2
        return $this->failedOften(self::LOGIN, self::LOGIN_FAILED);
60
    }
61
62 3
    public function updatePasswordFailedOften(): bool
63
    {
64 3
        return $this->failedOften(self::UPDATE_PASSWORD, self::UPDATE_PASSWORD_FAILED);
65
    }
66
67 5
    private function failedOften(string $success, string $failed): bool
68
    {
69 5
        if (PHP_SAPI === 'cli') {
70 5
            $ip = 'script';
71
        } else {
72
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
73
        }
74
75 5
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
76 5
            ->select('message')
77 5
            ->from('log')
78 5
            ->andWhere('priority = :priority')
79 5
            ->setParameter('priority', Logger::INFO)
80 5
            ->andWhere('message IN (:message)')
81 5
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
82 5
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
83 5
            ->andWhere('ip = :ip')
84 5
            ->setParameter('ip', $ip)
85 5
            ->orderBy('id', 'DESC');
86
87 5
        $events = $select->execute()->fetchAll(\PDO::FETCH_COLUMN);
88
89
        // Goes from present to past and count failure, until the last time we succeeded logging in
90 5
        $failureCount = 0;
91 5
        foreach ($events as $event) {
92 2
            if ($event === $success) {
93
                break;
94
            }
95 2
            ++$failureCount;
96
        }
97
98 5
        return $failureCount > 20;
99
    }
100
101
    /**
102
     * Delete log entries which are errors/warnings and older than one month
103
     * We always keep Logger::INFO level because we use it for statistics
104
     *
105
     * @return int the count deleted logs
106
     */
107 1
    public function deleteOldLogs(): int
108
    {
109 1
        $connection = $this->getEntityManager()->getConnection();
110 1
        $query = $connection->createQueryBuilder()
111 1
            ->delete('log')
112 1
            ->andWhere('log.priority != :priority OR message = :message')
113 1
            ->setParameter('priority', Logger::INFO)
114 1
            ->setParameter('message', self::LOGIN_FAILED)
115 1
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
116
117 1
        $connection->query('LOCK TABLES `log` WRITE;');
118 1
        $count = $query->execute();
119 1
        $connection->query('UNLOCK TABLES;');
120
121 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...
122
    }
123
124 1
    public function getLoginDate(User $user, bool $first): ?Chronos
125
    {
126 1
        $qb = $this->createQueryBuilder('log')
127 1
            ->select('log.creationDate')
128 1
            ->andWhere('log.creator = :user')
129 1
            ->andWhere('log.message = :message')
130 1
            ->setParameter('user', $user)
131 1
            ->setParameter('message', self::LOGIN)
132 1
            ->addOrderBy('log.creationDate', $first ? 'ASC' : 'DESC');
133
134 1
        $result = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
135
136 1
        return $result['creationDate'];
137
    }
138
}
139