Passed
Pull Request — master (#134)
by Damien
03:24
created

AuditReader::checkRoles()   C

Complexity

Conditions 12
Paths 48

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 15
nc 48
nop 2
dl 0
loc 35
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace DH\DoctrineAuditBundle\Reader;
4
5
use DH\DoctrineAuditBundle\Annotation\Security;
6
use DH\DoctrineAuditBundle\AuditConfiguration;
7
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
8
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
9
use DH\DoctrineAuditBundle\User\UserInterface;
10
use Doctrine\DBAL\Connection;
11
use Doctrine\DBAL\Query\QueryBuilder;
12
use Doctrine\DBAL\Statement;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
15
use PDO;
16
use Symfony\Component\Security\Core\Security as CoreSecurity;
17
18
class AuditReader
19
{
20
    public const UPDATE = 'update';
21
    public const ASSOCIATE = 'associate';
22
    public const DISSOCIATE = 'dissociate';
23
    public const INSERT = 'insert';
24
    public const REMOVE = 'remove';
25
26
    public const PAGE_SIZE = 50;
27
28
    /**
29
     * @var AuditConfiguration
30
     */
31
    private $configuration;
32
33
    /**
34
     * @var EntityManagerInterface
35
     */
36
    private $entityManager;
37
38
    /**
39
     * @var array
40
     */
41
    private $filters = [];
42
43
    /**
44
     * AuditReader constructor.
45
     *
46
     * @param AuditConfiguration     $configuration
47
     * @param EntityManagerInterface $entityManager
48
     */
49
    public function __construct(
50
        AuditConfiguration $configuration,
51
        EntityManagerInterface $entityManager
52
    ) {
53
        $this->configuration = $configuration;
54
        $this->entityManager = $entityManager;
55
    }
56
57
    /**
58
     * @return AuditConfiguration
59
     */
60
    public function getConfiguration(): AuditConfiguration
61
    {
62
        return $this->configuration;
63
    }
64
65
    /**
66
     * Set the filter(s) for AuditEntry retrieving.
67
     *
68
     * @param array|string $filter
69
     *
70
     * @return AuditReader
71
     */
72
    public function filterBy($filter): self
73
    {
74
        $filters = \is_array($filter) ? $filter : [$filter];
75
76
        $this->filters = array_filter($filters, static function ($f) {
77
            return \in_array($f, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true);
78
        });
79
80
        return $this;
81
    }
82
83
    /**
84
     * Returns current filter.
85
     *
86
     * @return array
87
     */
88
    public function getFilters(): array
89
    {
90
        return $this->filters;
91
    }
92
93
    /**
94
     * Returns an array of audit table names indexed by entity FQN.
95
     *
96
     * @throws \Doctrine\ORM\ORMException
97
     *
98
     * @return array
99
     */
100
    public function getEntities(): array
101
    {
102
        $metadataDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl();
103
        $entities = [];
104
        if (null !== $metadataDriver) {
105
            $entities = $metadataDriver->getAllClassNames();
106
        }
107
        $audited = [];
108
        foreach ($entities as $entity) {
109
            if ($this->configuration->isAuditable($entity)) {
110
                $audited[$entity] = $this->getEntityTableName($entity);
111
            }
112
        }
113
        ksort($audited);
114
115
        return $audited;
116
    }
117
118
    /**
119
     * Returns an array of audited entries/operations.
120
     *
121
     * @param object|string   $entity
122
     * @param null|int|string $id
123
     * @param null|int        $page
124
     * @param null|int        $pageSize
125
     * @param null|string     $transactionHash
126
     * @param bool            $strict
127
     *
128
     * @throws AccessDeniedException
129
     * @throws InvalidArgumentException
130
     *
131
     * @return array
132
     */
133
    public function getAudits($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): array
134
    {
135
        $this->checkAuditable($entity);
136
        $this->checkRoles($entity, Security::VIEW_SCOPE);
137
138
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict);
139
140
        /** @var Statement $statement */
141
        $statement = $queryBuilder->execute();
142
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
143
144
        return $statement->fetchAll();
145
    }
146
147
    /**
148
     * Returns an array of all audited entries/operations for a given transaction hash
149
     * indexed by entity FQCN.
150
     *
151
     * @param string $transactionHash
152
     *
153
     * @throws InvalidArgumentException
154
     * @throws \Doctrine\ORM\ORMException
155
     *
156
     * @return array
157
     */
158
    public function getAuditsByTransactionHash(string $transactionHash): array
159
    {
160
        $results = [];
161
162
        $entities = $this->getEntities();
163
        foreach ($entities as $entity => $tablename) {
164
            try {
165
                $audits = $this->getAudits($entity, null, null, null, $transactionHash);
166
                if (\count($audits) > 0) {
167
                    $results[$entity] = $audits;
168
                }
169
            } catch (AccessDeniedException $e) {
170
                // acces denied
171
            }
172
        }
173
174
        return $results;
175
    }
176
177
    /**
178
     * Returns an array of audited entries/operations.
179
     *
180
     * @param object|string   $entity
181
     * @param null|int|string $id
182
     * @param int             $page
183
     * @param int             $pageSize
184
     *
185
     * @throws AccessDeniedException
186
     * @throws InvalidArgumentException
187
     *
188
     * @return array
189
     */
190
    public function getAuditsPager($entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): array
191
    {
192
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
193
194
        $currentPage = $page < 1 ? 1 : $page;
195
        $firstResult = ($currentPage - 1) * $pageSize;
196
197
        $queryBuilder
198
            ->setFirstResult($firstResult)
199
            ->setMaxResults($pageSize)
200
        ;
201
202
        $paginator = new Paginator($queryBuilder);
203
        $numResults = $paginator->count();
204
        $hasPreviousPage = $currentPage > 1;
205
        $hasNextPage = ($currentPage * $pageSize) < $numResults;
206
207
        return [
208
            'results' => $paginator->getIterator(),
209
            'currentPage' => $currentPage,
210
            'hasPreviousPage' => $hasPreviousPage,
211
            'hasNextPage' => $hasNextPage,
212
            'previousPage' => $hasPreviousPage ? $currentPage - 1 : null,
213
            'nextPage' => $hasNextPage ? $currentPage + 1 : null,
214
            'numPages' => (int) ceil($numResults / $pageSize),
215
            'haveToPaginate' => $numResults > $pageSize,
216
        ];
217
    }
218
219
    /**
220
     * Returns the amount of audited entries/operations.
221
     *
222
     * @param object|string   $entity
223
     * @param null|int|string $id
224
     *
225
     * @throws AccessDeniedException
226
     * @throws InvalidArgumentException
227
     *
228
     * @return int
229
     */
230
    public function getAuditsCount($entity, $id = null): int
231
    {
232
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
233
234
        $result = $queryBuilder
235
            ->resetQueryPart('select')
236
            ->resetQueryPart('orderBy')
237
            ->select('COUNT(id)')
238
            ->execute()
239
            ->fetchColumn(0)
240
        ;
241
242
        return false === $result ? 0 : $result;
243
    }
244
245
    /**
246
     * @param object|string $entity
247
     * @param string        $id
248
     *
249
     * @throws AccessDeniedException
250
     * @throws InvalidArgumentException
251
     *
252
     * @return mixed[]
253
     */
254
    public function getAudit($entity, $id): array
255
    {
256
        $this->checkAuditable($entity);
257
        $this->checkRoles($entity, Security::VIEW_SCOPE);
258
259
        $connection = $this->entityManager->getConnection();
260
261
        /**
262
         * @var \Doctrine\DBAL\Query\QueryBuilder
263
         */
264
        $queryBuilder = $connection->createQueryBuilder();
265
        $queryBuilder
266
            ->select('*')
267
            ->from($this->getEntityAuditTableName($entity))
268
            ->where('id = :id')
269
            ->setParameter('id', $id)
270
        ;
271
272
        $this->filterByType($queryBuilder, $this->filters);
273
274
        /** @var Statement $statement */
275
        $statement = $queryBuilder->execute();
276
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
277
278
        return $statement->fetchAll();
279
    }
280
281
    /**
282
     * Returns the table name of $entity.
283
     *
284
     * @param object|string $entity
285
     *
286
     * @return string
287
     */
288
    public function getEntityTableName($entity): string
289
    {
290
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
291
292
        return $this->entityManager->getClassMetadata($entityName)->getTableName();
293
    }
294
295
    /**
296
     * Returns the audit table name for $entity.
297
     *
298
     * @param object|string $entity
299
     *
300
     * @return string
301
     */
302
    public function getEntityAuditTableName($entity): string
303
    {
304
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
305
306
        $schema = '';
307
        if ($this->entityManager->getClassMetadata($entityName)->getSchemaName()) {
308
            $schema = $this->entityManager->getClassMetadata($entityName)->getSchemaName().'.';
309
        }
310
311
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entityName), $this->configuration->getTableSuffix());
312
    }
313
314
    /**
315
     * @return EntityManagerInterface
316
     */
317
    public function getEntityManager(): EntityManagerInterface
318
    {
319
        return $this->entityManager;
320
    }
321
322
    private function filterByType(QueryBuilder $queryBuilder, array $filters): QueryBuilder
323
    {
324
        if (!empty($filters)) {
325
            $queryBuilder
326
                ->andWhere('type IN (:filters)')
327
                ->setParameter('filters', $filters, Connection::PARAM_STR_ARRAY)
328
            ;
329
        }
330
331
        return $queryBuilder;
332
    }
333
334
    private function filterByTransaction(QueryBuilder $queryBuilder, ?string $transactionHash): QueryBuilder
335
    {
336
        if (null !== $transactionHash) {
337
            $queryBuilder
338
                ->andWhere('transaction_hash = :transaction_hash')
339
                ->setParameter('transaction_hash', $transactionHash)
340
            ;
341
        }
342
343
        return $queryBuilder;
344
    }
345
346
    /**
347
     * @param QueryBuilder    $queryBuilder
348
     * @param null|int|string $id
349
     *
350
     * @return QueryBuilder
351
     */
352
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
353
    {
354
        if (null !== $id) {
355
            $queryBuilder
356
                ->andWhere('object_id = :object_id')
357
                ->setParameter('object_id', $id)
358
            ;
359
        }
360
361
        return $queryBuilder;
362
    }
363
364
    /**
365
     * Returns an array of audited entries/operations.
366
     *
367
     * @param object|string   $entity
368
     * @param null|int|string $id
369
     * @param null|int        $page
370
     * @param null|int        $pageSize
371
     * @param null|string     $transactionHash
372
     * @param bool            $strict
373
     *
374
     * @throws AccessDeniedException
375
     * @throws InvalidArgumentException
376
     *
377
     * @return QueryBuilder
378
     */
379
    private function getAuditsQueryBuilder($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): QueryBuilder
380
    {
381
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
382
383
        $this->checkAuditable($entity);
384
        $this->checkRoles($entity, Security::VIEW_SCOPE);
385
386
        if (null !== $page && $page < 1) {
387
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
388
        }
389
390
        if (null !== $pageSize && $pageSize < 1) {
391
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
392
        }
393
394
        $storage = $this->configuration->getEntityManager() ?? $this->entityManager;
395
        $connection = $storage->getConnection();
396
397
        $queryBuilder = $connection->createQueryBuilder();
398
        $queryBuilder
399
            ->select('*')
400
            ->from($this->getEntityAuditTableName($entity), 'at')
401
            ->orderBy('created_at', 'DESC')
402
            ->addOrderBy('id', 'DESC')
403
        ;
404
405
        $metadata = $this->entityManager->getClassMetadata($entityName);
406
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
407
            $queryBuilder
408
                ->andWhere('discriminator = :discriminator')
409
                ->setParameter('discriminator', \is_object($entity) ? \get_class($entity) : $entity)
410
            ;
411
        }
412
413
        $this->filterByObjectId($queryBuilder, $id);
414
        $this->filterByType($queryBuilder, $this->filters);
415
        $this->filterByTransaction($queryBuilder, $transactionHash);
416
417
        if (null !== $pageSize) {
418
            $queryBuilder
419
                ->setFirstResult(($page - 1) * $pageSize)
420
                ->setMaxResults($pageSize)
421
            ;
422
        }
423
424
        return $queryBuilder;
425
    }
426
427
    /**
428
     * Throws an InvalidArgumentException if given entity is not auditable.
429
     *
430
     * @param object|string $entity
431
     *
432
     * @throws InvalidArgumentException
433
     */
434
    private function checkAuditable($entity): void
435
    {
436
        if (!$this->configuration->isAuditable($entity)) {
437
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
0 ignored issues
show
Bug introduced by
Are you sure $entity of type object|string can be used in concatenation? ( Ignorable by Annotation )

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

437
            throw new InvalidArgumentException('Entity './** @scrutinizer ignore-type */ $entity.' is not auditable.');
Loading history...
438
        }
439
    }
440
441
    /**
442
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
443
     *
444
     * @param object|string $entity
445
     * @param string        $scope
446
     *
447
     * @throws AccessDeniedException
448
     */
449
    private function checkRoles($entity, string $scope): void
450
    {
451
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
452
453
        $userProvider = $this->configuration->getUserProvider();
454
        $user = null === $userProvider ? null : $userProvider->getUser();
455
        $security = null === $userProvider ? null : $userProvider->getSecurity();
456
457
        if (!($user instanceof UserInterface) || !($security instanceof CoreSecurity)) {
458
            // If no security defined or no user identified, consider access granted
459
            return;
460
        }
461
462
        $entities = $this->configuration->getEntities();
463
464
        if (!isset($entities[$entityName]['roles']) || null === $entities[$entityName]['roles']) {
465
            // If no roles are configured, consider access granted
466
            return;
467
        }
468
469
        if (!isset($entities[$entityName]['roles'][$scope]) || null === $entities[$entityName]['roles'][$scope]) {
470
            // If no roles for the given scope are configured, consider access granted
471
            return;
472
        }
473
474
        // roles are defined for the give scope
475
        foreach ($entities[$entityName]['roles'][$scope] as $role) {
476
            if ($security->isGranted($role)) {
477
                // role granted => access granted
478
                return;
479
            }
480
        }
481
482
        // access denied
483
        throw new AccessDeniedException('You are not allowed to access audits of '.$entityName.' entity.');
484
    }
485
}
486