Passed
Push — master ( 2111d5...99ee46 )
by Damien
02:33
created

AuditReader::filterBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

463
        throw new AccessDeniedException('You are not allowed to access audits of './** @scrutinizer ignore-type */ $entity.' entity.');
Loading history...
464
    }
465
}
466