|
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(); |
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
123
|
|
|
$connection->executeStatement('UNLOCK TABLES;'); |
|
124
|
|
|
|
|
125
|
|
|
return $count; |
|
|
|
|
|
|
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
|
|
|
|
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.