Failed Conditions
Push — master ( a8fd5c...ef1a3d )
by Adrien
11:53
created

LogRepository::getAccessibleSubQuery()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 17
ccs 0
cts 8
cp 0
crap 12
rs 10
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 implements LimitedAccessSubQueryInterface
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
     * Log message to be used when a door is opened
41
     */
42
    const DOOR_OPENED = 'door opened: ';
43
44
    /**
45
     * This should NOT be called directly, instead use `_log()` to log stuff
46
     *
47
     * @param array $event
48
     */
49 11
    public function log(array $event): void
50
    {
51 11
        $event['creation_date'] = Chronos::instance($event['creation_date'])->toIso8601String();
52 11
        $event['extra'] = json_encode($event['extra']);
53
54 11
        $this->getEntityManager()->getConnection()->insert('log', $event);
55 11
    }
56
57
    /**
58
     * Returns whether the current IP often failed to login
59
     *
60
     * @return bool
61
     */
62 2
    public function loginFailedOften(): bool
63
    {
64 2
        return $this->failedOften(self::LOGIN, self::LOGIN_FAILED);
65
    }
66
67 3
    public function updatePasswordFailedOften(): bool
68
    {
69 3
        return $this->failedOften(self::UPDATE_PASSWORD, self::UPDATE_PASSWORD_FAILED);
70
    }
71
72 5
    private function failedOften(string $success, string $failed): bool
73
    {
74 5
        if (PHP_SAPI === 'cli') {
75 5
            $ip = 'script';
76
        } else {
77
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
78
        }
79
80 5
        $select = $this->getEntityManager()->getConnection()->createQueryBuilder()
81 5
            ->select('message')
82 5
            ->from('log')
83 5
            ->andWhere('priority = :priority')
84 5
            ->setParameter('priority', Logger::INFO)
85 5
            ->andWhere('message IN (:message)')
86 5
            ->setParameter('message', [$success, $failed], Connection::PARAM_STR_ARRAY)
87 5
            ->andWhere('creation_date > DATE_SUB(NOW(), INTERVAL 30 MINUTE)')
88 5
            ->andWhere('ip = :ip')
89 5
            ->setParameter('ip', $ip)
90 5
            ->orderBy('id', 'DESC');
91
92 5
        $events = $select->execute()->fetchAll(\PDO::FETCH_COLUMN);
93
94
        // Goes from present to past and count failure, until the last time we succeeded logging in
95 5
        $failureCount = 0;
96 5
        foreach ($events as $event) {
97 2
            if ($event === $success) {
98
                break;
99
            }
100 2
            ++$failureCount;
101
        }
102
103 5
        return $failureCount > 20;
104
    }
105
106
    /**
107
     * Delete log entries which are errors/warnings and older than one month
108
     * We always keep Logger::INFO level because we use it for statistics
109
     *
110
     * @return int the count deleted logs
111
     */
112 1
    public function deleteOldLogs(): int
113
    {
114 1
        $connection = $this->getEntityManager()->getConnection();
115 1
        $query = $connection->createQueryBuilder()
116 1
            ->delete('log')
117 1
            ->andWhere('log.priority != :priority OR message = :message')
118 1
            ->setParameter('priority', Logger::INFO)
119 1
            ->setParameter('message', self::LOGIN_FAILED)
120 1
            ->andWhere('log.creation_date < DATE_SUB(NOW(), INTERVAL 1 MONTH)');
121
122 1
        $connection->query('LOCK TABLES `log` WRITE;');
123 1
        $count = $query->execute();
124 1
        $connection->query('UNLOCK TABLES;');
125
126 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...
127
    }
128
129 1
    public function getLoginDate(User $user, bool $first): ?Chronos
130
    {
131 1
        $qb = $this->createQueryBuilder('log')
132 1
            ->select('log.creationDate')
133 1
            ->andWhere('log.creator = :user')
134 1
            ->andWhere('log.message = :message')
135 1
            ->setParameter('user', $user)
136 1
            ->setParameter('message', self::LOGIN)
137 1
            ->addOrderBy('log.creationDate', $first ? 'ASC' : 'DESC');
138
139
        $result = $this->getAclFilter()->runWithoutAcl(function () use ($qb) {
140 1
            return $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
141 1
        });
142
143 1
        return $result['creationDate'];
144
    }
145
146
    /**
147
     * Returns pure SQL to get ID of all objects that are accessible to given user.
148
     *
149
     * @param null|User $user
150
     *
151
     * @return string
152
     */
153
    public function getAccessibleSubQuery(?User $user): string
154
    {
155
        if (!$user) {
156
            return '-1';
157
        }
158
159
        // Sysops and responsible can read all logs
160
        if (in_array($user->getRole(), [User::ROLE_RESPONSIBLE, User::ROLE_ADMINISTRATOR], true)) {
161
            return $this->getAllIdsQuery();
162
        }
163
164
        $subquery = '
165
            SELECT log.id FROM `log` WHERE
166
            message LIKE ' . $this->getEntityManager()->getConnection()->quote(self::DOOR_OPENED . '%') . '
167
            AND log.creator_id = ' . $user->getId();
168
169
        return $subquery;
170
    }
171
}
172