Failed Conditions
Push — master ( e75200...fab761 )
by Adrien
02:43
created

src/Repository/Traits/LogRepository.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Repository\Traits;
6
7
use Cake\Chronos\Chronos;
1 ignored issue
show
The type Cake\Chronos\Chronos was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Doctrine\DBAL\ArrayParameterType;
9
use Doctrine\ORM\EntityManager;
10
use Doctrine\ORM\QueryBuilder;
11
use Ecodev\Felix\Repository\LogRepository as LogRepositoryInterface;
12
use Laminas\Log\Logger;
13
14
trait LogRepository
15
{
16
    /**
17
     * @return EntityManager
18
     */
19
    abstract protected function getEntityManager();
20
21
    /**
22
     * Creates a new QueryBuilder instance that is pre-populated for this entity name.
23
     *
24
     * @param string $alias
25
     * @param null|string $indexBy the index for the from
26
     *
27
     * @return QueryBuilder
28
     */
29
    abstract public function createQueryBuilder($alias, $indexBy = null);
30
31
    /**
32
     * This should NOT be called directly, instead use `_log()` to log stuff.
33
     */
34
    public function log(array $event): void
35
    {
36
        $event['creation_date'] = Chronos::instance($event['timestamp'])->toDateTimeString();
37
        $event['extra'] = json_encode($event['extra'], JSON_THROW_ON_ERROR);
38
        unset($event['timestamp'], $event['priorityName'], $event['login']);
39
40
        $this->getEntityManager()->getConnection()->insert('log', $event);
41
    }
42
43
    /**
44
     * Returns whether the current IP often failed to log in.
45
     */
46
    public function loginFailedOften(): bool
47
    {
48
        return $this->failedOften(LogRepositoryInterface::LOGIN, LogRepositoryInterface::LOGIN_FAILED);
49
    }
50
51
    public function updatePasswordFailedOften(): bool
52
    {
53
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::UPDATE_PASSWORD_FAILED);
54
    }
55
56
    public function requestPasswordResetOften(): bool
57
    {
58
        return $this->failedOften(LogRepositoryInterface::UPDATE_PASSWORD, LogRepositoryInterface::REQUEST_PASSWORD_RESET, 10);
59
    }
60
61
    public function registerOften(): bool
62
    {
63
        return $this->failedOften(LogRepositoryInterface::REGISTER_CONFIRM, LogRepositoryInterface::REGISTER, 10);
64
    }
65
66
    private function failedOften(string $success, string $failed, int $maxFailureCount = 20): bool
67
    {
68
        if (PHP_SAPI === 'cli') {
69
            $ip = !empty(getenv('REMOTE_ADDR')) ? getenv('REMOTE_ADDR') : 'script';
70
        } else {
71
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
72
        }
73
74
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
75
            ->select('message')
76
            ->from('log')
77
            ->andWhere('priority = :priority')
78
            ->setParameter('priority', Logger::INFO)
79
            ->andWhere('message IN (:message)')
80
            ->setParameter('message', [$success, $failed], ArrayParameterType::STRING)
81
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
82
            ->andWhere('ip = :ip')
83
            ->setParameter('ip', $ip)
84
            ->orderBy('id', 'DESC');
85
86
        $events = $select->executeQuery()->fetchFirstColumn();
87
88
        // Goes from present to past and count failure, until the last time we succeeded logging in
89
        $failureCount = 0;
90
        foreach ($events as $event) {
91
            if ($event === $success) {
92
                break;
93
            }
94
            ++$failureCount;
95
        }
96
97
        return $failureCount > $maxFailureCount;
98
    }
99
100
    /**
101
     * Delete log entries which are errors/warnings and older than two months
102
     * We always keep Logger::INFO level because we use it for statistics.
103
     *
104
     * @return int the count deleted logs
105
     */
106
    public function deleteOldLogs(): int
107
    {
108
        $connection = $this->getEntityManager()->getConnection();
109
        $query = $connection->createQueryBuilder()
110
            ->delete('log')
111
            ->andWhere('log.priority != :priority OR message IN (:message)')
112
            ->setParameter('priority', Logger::INFO)
113
            ->setParameter('message', [
114
                LogRepositoryInterface::LOGIN,
115
                LogRepositoryInterface::LOGIN_FAILED,
116
                LogRepositoryInterface::UPDATE_PASSWORD,
117
                LogRepositoryInterface::REQUEST_PASSWORD_RESET,
118
                LogRepositoryInterface::UPDATE_PASSWORD_FAILED,
119
                LogRepositoryInterface::REGISTER,
120
                LogRepositoryInterface::REGISTER_CONFIRM,
121
            ], ArrayParameterType::STRING)
122
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 2 MONTH)');
123
124
        $connection->executeStatement('LOCK TABLES `log` WRITE;');
125
        $count = $query->executeStatement();
126
        $connection->executeStatement('UNLOCK TABLES;');
127
128
        return $count;
129
    }
130
}
131