Passed
Push — master ( 89f24e...8c4dad )
by Sylvain
08:05
created

LogRepository::requestPasswordResetOften()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
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 Doctrine\ORM\EntityManager;
10
use Doctrine\ORM\QueryBuilder;
11
use Ecodev\Felix\Model\User;
12
use Ecodev\Felix\Repository\LogRepository as LogRepositoryInterface;
13
use Laminas\Log\Logger;
14
use PDO;
15
16
trait LogRepository
17
{
18
    /**
19
     * @return EntityManager
20
     */
21
    abstract protected function getEntityManager();
22
23
    /**
24
     * Creates a new QueryBuilder instance that is prepopulated for this entity name.
25
     *
26
     * @param string $alias
27
     * @param string $indexBy the index for the from
28
     *
29
     * @return QueryBuilder
30
     */
31
    abstract public function createQueryBuilder($alias, $indexBy = null);
32
33
    /**
34
     * This should NOT be called directly, instead use `_log()` to log stuff
35
     */
36
    public function log(array $event): void
37
    {
38
        $event['creation_date'] = Chronos::instance($event['timestamp'])->toIso8601String();
39
        $event['extra'] = json_encode($event['extra']);
40
        unset($event['timestamp'], $event['priorityName'], $event['login']);
41
42
        $this->getEntityManager()->getConnection()->insert('log', $event);
43
    }
44
45
    /**
46
     * Returns whether the current IP often failed to login
47
     */
48
    public function loginFailedOften(): bool
49
    {
50
        return $this->failedOften(LogRepositoryInterface::LOGIN, LogRepositoryInterface::LOGIN_FAILED);
51
    }
52
53
    public function updatePasswordFailedOften(): bool
54
    {
55
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::UPDATE_PASSWORD_FAILED);
56
    }
57
58
    public function requestPasswordResetOften(): bool
59
    {
60
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::REQUEST_PASSWORD_RESET, 10);
61
    }
62
63
    public function registerOften(): bool
64
    {
65
        return $this->failedOften(LogRepositoryInterface::REGISTER_CONFIRM, LogRepositoryInterface::REGISTER, 10);
66
    }
67
68
    private function failedOften(string $success, string $failed, int $maxFailureCount = 20): bool
69
    {
70
        if (PHP_SAPI === 'cli') {
71
            $ip = 'script';
72
        } else {
73
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
74
        }
75
76
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
77
            ->select('message')
78
            ->from('log')
79
            ->andWhere('priority = :priority')
80
            ->setParameter('priority', Logger::INFO)
81
            ->andWhere('message IN (:message)')
82
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
83
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
84
            ->andWhere('ip = :ip')
85
            ->setParameter('ip', $ip)
86
            ->orderBy('id', 'DESC');
87
88
        $events = $select->execute()->fetchAll(PDO::FETCH_COLUMN);
89
90
        // Goes from present to past and count failure, until the last time we succeeded logging in
91
        $failureCount = 0;
92
        foreach ($events as $event) {
93
            if ($event === $success) {
94
                break;
95
            }
96
            ++$failureCount;
97
        }
98
99
        return $failureCount > $maxFailureCount;
100
    }
101
102
    /**
103
     * Delete log entries which are errors/warnings and older than one month
104
     * We always keep Logger::INFO level because we use it for statistics
105
     *
106
     * @return int the count deleted logs
107
     */
108
    public function deleteOldLogs(): int
109
    {
110
        $connection = $this->getEntityManager()->getConnection();
111
        $query = $connection->createQueryBuilder()
112
            ->delete('log')
113
            ->andWhere('log.priority != :priority OR message IN (:message)')
114
            ->setParameter('priority', Logger::INFO)
115
            ->setParameter('message', [
116
                LogRepositoryInterface::LOGIN_FAILED,
117
                LogRepositoryInterface::REQUEST_PASSWORD_RESET,
118
                LogRepositoryInterface::REGISTER,
119
            ], Connection::PARAM_STR_ARRAY)
120
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
121
122
        $connection->query('LOCK TABLES `log` WRITE;');
123
        $count = $query->execute();
124
        $connection->query('UNLOCK TABLES;');
125
126
        return $count;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $count returns the type Doctrine\DBAL\Driver\Statement which is incompatible with the type-hinted return integer.
Loading history...
127
    }
128
129
    public function getLoginDate(User $user, bool $first): ?Chronos
130
    {
131
        $qb = $this->createQueryBuilder('log')
132
            ->select('log.creationDate')
133
            ->andWhere('log.creator = :user')
134
            ->andWhere('log.message = :message')
135
            ->setParameter('user', $user)
136
            ->setParameter('message', LogRepositoryInterface::LOGIN)
137
            ->addOrderBy('log.creationDate', $first ? 'ASC' : 'DESC');
138
139
        $result = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
140
141
        return $result['creationDate'] ?? null;
142
    }
143
}
144