Passed
Push — master ( ecadea...14db22 )
by Damien
03:21
created

Reader::getEntityTableName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace DH\DoctrineAuditBundle\Reader;
4
5
use DateTime;
6
use DH\DoctrineAuditBundle\Annotation\Security;
7
use DH\DoctrineAuditBundle\Configuration;
8
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
9
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
10
use DH\DoctrineAuditBundle\User\UserInterface;
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Query\QueryBuilder;
13
use Doctrine\DBAL\Statement;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
16
use PDO;
17
use Symfony\Component\Security\Core\Security as CoreSecurity;
18
19
class Reader
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 Configuration
31
     */
32
    private $configuration;
33
34
    /**
35
     * @var EntityManagerInterface
36
     */
37
    private $entityManager;
38
39
    /**
40
     * @var array
41
     */
42
    private $filters = [];
43
44
    /**
45
     * AuditReader constructor.
46
     *
47
     * @param Configuration          $configuration
48
     * @param EntityManagerInterface $entityManager
49
     */
50
    public function __construct(
51
        Configuration $configuration,
52
        EntityManagerInterface $entityManager
53
    ) {
54
        $this->configuration = $configuration;
55
        $this->entityManager = $entityManager;
56
    }
57
58
    /**
59
     * @return Configuration
60
     */
61
    public function getConfiguration(): Configuration
62
    {
63
        return $this->configuration;
64
    }
65
66
    /**
67
     * Set the filter(s) for AuditEntry retrieving.
68
     *
69
     * @param array|string $filter
70
     *
71
     * @return Reader
72
     */
73
    public function filterBy($filter): self
74
    {
75
        $filters = \is_array($filter) ? $filter : [$filter];
76
77
        $this->filters = array_filter($filters, static function ($f) {
78
            return \in_array($f, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true);
79
        });
80
81
        return $this;
82
    }
83
84
    /**
85
     * Returns current filter.
86
     *
87
     * @return array
88
     */
89
    public function getFilters(): array
90
    {
91
        return $this->filters;
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 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(string $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, Entry::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 string          $entity
182
     * @param null|int|string $id
183
     * @param null|DateTime   $startDate       - Expected in configured timezone
184
     * @param null|DateTime   $endDate         - Expected in configured timezone
185
     * @param null|int        $page
186
     * @param null|int        $pageSize
187
     * @param null|string     $transactionHash
188
     * @param bool            $strict
189
     *
190
     * @throws AccessDeniedException
191
     * @throws InvalidArgumentException
192
     *
193
     * @return array
194
     */
195
    public function getAuditsByDate(string $entity, $id = null, ?DateTime $startDate = null, ?DateTime $endDate = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): array
196
    {
197
        $this->checkAuditable($entity);
198
        $this->checkRoles($entity, Security::VIEW_SCOPE);
199
200
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict, $startDate, $endDate);
201
202
        /** @var Statement $statement */
203
        $statement = $queryBuilder->execute();
204
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
205
206
        return $statement->fetchAll();
207
    }
208
209
    /**
210
     * Returns an array of audited entries/operations.
211
     *
212
     * @param string          $entity
213
     * @param null|int|string $id
214
     * @param int             $page
215
     * @param int             $pageSize
216
     *
217
     * @throws AccessDeniedException
218
     * @throws InvalidArgumentException
219
     *
220
     * @return array
221
     */
222
    public function getAuditsPager(string $entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): array
223
    {
224
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize);
225
226
        $paginator = new Paginator($queryBuilder);
227
        $numResults = $paginator->count();
228
229
        $currentPage = $page < 1 ? 1 : $page;
230
        $hasPreviousPage = $currentPage > 1;
231
        $hasNextPage = ($currentPage * $pageSize) < $numResults;
232
233
        return [
234
            'results' => $paginator->getIterator(),
235
            'currentPage' => $currentPage,
236
            'hasPreviousPage' => $hasPreviousPage,
237
            'hasNextPage' => $hasNextPage,
238
            'previousPage' => $hasPreviousPage ? $currentPage - 1 : null,
239
            'nextPage' => $hasNextPage ? $currentPage + 1 : null,
240
            'numPages' => (int) ceil($numResults / $pageSize),
241
            'haveToPaginate' => $numResults > $pageSize,
242
        ];
243
    }
244
245
    /**
246
     * Returns the amount of audited entries/operations.
247
     *
248
     * @param string          $entity
249
     * @param null|int|string $id
250
     *
251
     * @throws AccessDeniedException
252
     * @throws InvalidArgumentException
253
     *
254
     * @return int
255
     */
256
    public function getAuditsCount(string $entity, $id = null): int
257
    {
258
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
259
260
        $result = $queryBuilder
261
            ->resetQueryPart('select')
262
            ->resetQueryPart('orderBy')
263
            ->select('COUNT(id)')
264
            ->execute()
265
            ->fetchColumn(0)
266
        ;
267
268
        return false === $result ? 0 : $result;
269
    }
270
271
    /**
272
     * @param string $entity
273
     * @param string $id
274
     *
275
     * @throws AccessDeniedException
276
     * @throws InvalidArgumentException
277
     *
278
     * @return mixed[]
279
     */
280
    public function getAudit(string $entity, $id): array
281
    {
282
        $this->checkAuditable($entity);
283
        $this->checkRoles($entity, Security::VIEW_SCOPE);
284
285
        $connection = $this->entityManager->getConnection();
286
287
        /**
288
         * @var \Doctrine\DBAL\Query\QueryBuilder
289
         */
290
        $queryBuilder = $connection->createQueryBuilder();
291
        $queryBuilder
292
            ->select('*')
293
            ->from($this->getEntityAuditTableName($entity))
294
            ->where('id = :id')
295
            ->setParameter('id', $id)
296
        ;
297
298
        $this->filterByType($queryBuilder, $this->filters);
299
300
        /** @var Statement $statement */
301
        $statement = $queryBuilder->execute();
302
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
303
304
        return $statement->fetchAll();
305
    }
306
307
    /**
308
     * Returns the table name of $entity.
309
     *
310
     * @param string $entity
311
     *
312
     * @return string
313
     */
314
    public function getEntityTableName(string $entity): string
315
    {
316
        return $this->entityManager->getClassMetadata($entity)->getTableName();
317
    }
318
319
    /**
320
     * Returns the audit table name for $entity.
321
     *
322
     * @param string $entity
323
     *
324
     * @return string
325
     */
326
    public function getEntityAuditTableName(string $entity): string
327
    {
328
        $schema = '';
329
        if ($this->entityManager->getClassMetadata($entity)->getSchemaName()) {
330
            $schema = $this->entityManager->getClassMetadata($entity)->getSchemaName().'.';
331
        }
332
333
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entity), $this->configuration->getTableSuffix());
334
    }
335
336
    /**
337
     * @return EntityManagerInterface
338
     */
339
    public function getEntityManager(): EntityManagerInterface
340
    {
341
        return $this->entityManager;
342
    }
343
344
    private function filterByType(QueryBuilder $queryBuilder, array $filters): QueryBuilder
345
    {
346
        if (!empty($filters)) {
347
            $queryBuilder
348
                ->andWhere('type IN (:filters)')
349
                ->setParameter('filters', $filters, Connection::PARAM_STR_ARRAY)
350
            ;
351
        }
352
353
        return $queryBuilder;
354
    }
355
356
    private function filterByTransaction(QueryBuilder $queryBuilder, ?string $transactionHash): QueryBuilder
357
    {
358
        if (null !== $transactionHash) {
359
            $queryBuilder
360
                ->andWhere('transaction_hash = :transaction_hash')
361
                ->setParameter('transaction_hash', $transactionHash)
362
            ;
363
        }
364
365
        return $queryBuilder;
366
    }
367
368
    private function filterByDate(QueryBuilder $queryBuilder, ?DateTime $startDate, ?DateTime $endDate): QueryBuilder
369
    {
370
        if (null !== $startDate && null !== $endDate && $endDate < $startDate) {
371
            throw new \InvalidArgumentException('$endDate must be greater than $startDate.');
372
        }
373
374
        if (null !== $startDate) {
375
            $queryBuilder
376
                ->andWhere('created_at >= :start_date')
377
                ->setParameter('start_date', $startDate->format('Y-m-d H:i:s'))
378
            ;
379
        }
380
381
        if (null !== $endDate) {
382
            $queryBuilder
383
                ->andWhere('created_at <= :end_date')
384
                ->setParameter('end_date', $endDate->format('Y-m-d H:i:s'))
385
            ;
386
        }
387
388
        return $queryBuilder;
389
    }
390
391
    /**
392
     * @param QueryBuilder    $queryBuilder
393
     * @param null|int|string $id
394
     *
395
     * @return QueryBuilder
396
     */
397
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
398
    {
399
        if (null !== $id) {
400
            $queryBuilder
401
                ->andWhere('object_id = :object_id')
402
                ->setParameter('object_id', $id)
403
            ;
404
        }
405
406
        return $queryBuilder;
407
    }
408
409
    /**
410
     * Returns an array of audited entries/operations.
411
     *
412
     * @param string          $entity
413
     * @param null|int|string $id
414
     * @param null|int        $page
415
     * @param null|int        $pageSize
416
     * @param null|string     $transactionHash
417
     * @param bool            $strict
418
     * @param null|DateTime   $startDate
419
     * @param null|DateTime   $endDate
420
     *
421
     * @throws AccessDeniedException
422
     * @throws InvalidArgumentException
423
     *
424
     * @return QueryBuilder
425
     */
426
    private function getAuditsQueryBuilder(string $entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true, ?DateTime $startDate = null, ?DateTime $endDate = null): QueryBuilder
427
    {
428
        $this->checkAuditable($entity);
429
        $this->checkRoles($entity, Security::VIEW_SCOPE);
430
431
        if (null !== $page && $page < 1) {
432
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
433
        }
434
435
        if (null !== $pageSize && $pageSize < 1) {
436
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
437
        }
438
439
        $storage = $this->configuration->getEntityManager() ?? $this->entityManager;
440
        $connection = $storage->getConnection();
441
442
        $queryBuilder = $connection->createQueryBuilder();
443
        $queryBuilder
444
            ->select('*')
445
            ->from($this->getEntityAuditTableName($entity), 'at')
446
            ->orderBy('created_at', 'DESC')
447
            ->addOrderBy('id', 'DESC')
448
        ;
449
450
        $metadata = $this->entityManager->getClassMetadata($entity);
451
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
452
            $queryBuilder
453
                ->andWhere('discriminator = :discriminator')
454
                ->setParameter('discriminator', $entity)
455
            ;
456
        }
457
458
        $this->filterByObjectId($queryBuilder, $id);
459
        $this->filterByType($queryBuilder, $this->filters);
460
        $this->filterByTransaction($queryBuilder, $transactionHash);
461
        $this->filterByDate($queryBuilder, $startDate, $endDate);
462
463
        if (null !== $pageSize) {
464
            $queryBuilder
465
                ->setFirstResult(($page - 1) * $pageSize)
466
                ->setMaxResults($pageSize)
467
            ;
468
        }
469
470
        return $queryBuilder;
471
    }
472
473
    /**
474
     * Throws an InvalidArgumentException if given entity is not auditable.
475
     *
476
     * @param string $entity
477
     *
478
     * @throws InvalidArgumentException
479
     */
480
    private function checkAuditable(string $entity): void
481
    {
482
        if (!$this->configuration->isAuditable($entity)) {
483
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
484
        }
485
    }
486
487
    /**
488
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
489
     *
490
     * @param string $entity
491
     * @param string $scope
492
     *
493
     * @throws AccessDeniedException
494
     */
495
    private function checkRoles(string $entity, string $scope): void
496
    {
497
        $userProvider = $this->configuration->getUserProvider();
498
        $user = null === $userProvider ? null : $userProvider->getUser();
499
        $security = null === $userProvider ? null : $userProvider->getSecurity();
500
501
        if (!($user instanceof UserInterface) || !($security instanceof CoreSecurity)) {
502
            // If no security defined or no user identified, consider access granted
503
            return;
504
        }
505
506
        $entities = $this->configuration->getEntities();
507
508
        $roles = $entities[$entity]['roles'] ?? null;
509
510
        if (null === $roles) {
511
            // If no roles are configured, consider access granted
512
            return;
513
        }
514
515
        $scope = $roles[$scope] ?? null;
516
517
        if (null === $scope) {
518
            // If no roles for the given scope are configured, consider access granted
519
            return;
520
        }
521
522
        // roles are defined for the give scope
523
        foreach ($scope as $role) {
524
            if ($security->isGranted($role)) {
525
                // role granted => access granted
526
                return;
527
            }
528
        }
529
530
        // access denied
531
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
532
    }
533
}
534