Failed Conditions
Pull Request — master (#11)
by Adrien
15:48 queued 12:33
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 2
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
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
     * @param string $alias
26
     * @param string $indexBy the index for the from
27
     *
28
     * @return QueryBuilder
29
     */
30
    abstract public function createQueryBuilder($alias, $indexBy = null);
31
32
    /**
33
     * This should NOT be called directly, instead use `_log()` to log stuff.
34
     */
35
    public function log(array $event): void
36
    {
37
        $event['creation_date'] = Chronos::instance($event['timestamp'])->toIso8601String();
38
        $event['extra'] = json_encode($event['extra'], JSON_THROW_ON_ERROR);
39
        unset($event['timestamp'], $event['priorityName'], $event['login']);
40
41
        $this->getEntityManager()->getConnection()->insert('log', $event);
42
    }
43
44
    /**
45
     * Returns whether the current IP often failed to login.
46
     */
47
    public function loginFailedOften(): bool
48
    {
49
        return $this->failedOften(LogRepositoryInterface::LOGIN, LogRepositoryInterface::LOGIN_FAILED);
50
    }
51
52
    public function updatePasswordFailedOften(): bool
53
    {
54
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::UPDATE_PASSWORD_FAILED);
55
    }
56
57
    public function requestPasswordResetOften(): bool
58
    {
59
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::REQUEST_PASSWORD_RESET, 10);
60
    }
61
62
    public function registerOften(): bool
63
    {
64
        return $this->failedOften(LogRepositoryInterface::REGISTER_CONFIRM, LogRepositoryInterface::REGISTER, 10);
65
    }
66
67
    private function failedOften(string $success, string $failed, int $maxFailureCount = 20): bool
68
    {
69
        if (PHP_SAPI === 'cli') {
70
            $ip = !empty(getenv('REMOTE_ADDR')) ? getenv('REMOTE_ADDR') : 'script';
71
        } else {
72
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
73
        }
74
75
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
76
            ->select('message')
77
            ->from('log')
78
            ->andWhere('priority = :priority')
79
            ->setParameter('priority', Logger::INFO)
80
            ->andWhere('message IN (:message)')
81
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
82
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
83
            ->andWhere('ip = :ip')
84
            ->setParameter('ip', $ip)
85
            ->orderBy('id', 'DESC');
86
87
        $events = $select->execute()->fetchFirstColumn();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Query\QueryBuilder::execute() has been deprecated: Use {@see executeQuery()} or {@see executeStatement()} instead. ( Ignorable by Annotation )

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

87
        $events = /** @scrutinizer ignore-deprecated */ $select->execute()->fetchFirstColumn();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
88
89
        // Goes from present to past and count failure, until the last time we succeeded logging in
90
        $failureCount = 0;
91
        foreach ($events as $event) {
92
            if ($event === $success) {
93
                break;
94
            }
95
            ++$failureCount;
96
        }
97
98
        return $failureCount > $maxFailureCount;
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
    public function deleteOldLogs(): int
108
    {
109
        $connection = $this->getEntityManager()->getConnection();
110
        $query = $connection->createQueryBuilder()
111
            ->delete('log')
112
            ->andWhere('log.priority != :priority OR message IN (:message)')
113
            ->setParameter('priority', Logger::INFO)
114
            ->setParameter('message', [
115
                LogRepositoryInterface::LOGIN_FAILED,
116
                LogRepositoryInterface::REQUEST_PASSWORD_RESET,
117
                LogRepositoryInterface::REGISTER,
118
            ], Connection::PARAM_STR_ARRAY)
119
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
120
121
        $connection->executeStatement('LOCK TABLES `log` WRITE;');
122
        $count = $query->execute();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Query\QueryBuilder::execute() has been deprecated: Use {@see executeQuery()} or {@see executeStatement()} instead. ( Ignorable by Annotation )

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

122
        $count = /** @scrutinizer ignore-deprecated */ $query->execute();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
123
        $connection->executeStatement('UNLOCK TABLES;');
124
125
        return $count;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $count could return the type Doctrine\DBAL\Result which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
126
    }
127
128
    public function getLoginDate(User $user, bool $first): ?Chronos
129
    {
130
        $qb = $this->createQueryBuilder('log')
131
            ->select('log.creationDate')
132
            ->andWhere('log.creator = :user')
133
            ->andWhere('log.message = :message')
134
            ->setParameter('user', $user)
135
            ->setParameter('message', LogRepositoryInterface::LOGIN)
136
            ->addOrderBy('log.creationDate', $first ? 'ASC' : 'DESC');
137
138
        $result = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
139
140
        return $result['creationDate'] ?? null;
141
    }
142
}
143