Completed
Push — ezp_31107 ( 816f32...9177e0 )
by
unknown
220:54 queued 209:22
created

DoctrineDatabase::fetchUserGroups()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 24
rs 9.536
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
declare(strict_types=1);
8
9
namespace eZ\Publish\Core\Persistence\Legacy\User\Role\Gateway;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\FetchMode;
13
use Doctrine\DBAL\ParameterType;
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use eZ\Publish\Core\Persistence\Legacy\User\Role\Gateway;
16
use eZ\Publish\SPI\Persistence\User\Policy;
17
use eZ\Publish\SPI\Persistence\User\RoleUpdateStruct;
18
use eZ\Publish\SPI\Persistence\User\Role;
19
20
/**
21
 * User Role gateway implementation using the Doctrine database.
22
 *
23
 * @internal Gateway implementation is considered internal. Use Persistence User Handler instead.
24
 *
25
 * @see \eZ\Publish\SPI\Persistence\User\Handler
26
 */
27
final class DoctrineDatabase extends Gateway
28
{
29
    /** @var \Doctrine\DBAL\Connection */
30
    private $connection;
31
32
    /** @var \Doctrine\DBAL\Platforms\AbstractPlatform */
33
    private $dbPlatform;
34
35
    /**
36
     * Construct from database handler.
37
     *
38
     * @throws \Doctrine\DBAL\DBALException
39
     */
40
    public function __construct(Connection $connection)
41
    {
42
        $this->connection = $connection;
43
        $this->dbPlatform = $this->connection->getDatabasePlatform();
44
    }
45
46
    public function createRole(Role $role): Role
47
    {
48
        // Role original ID is set when creating a draft from an existing role
49
        if ($role->status === Role::STATUS_DRAFT && $role->id) {
50
            $roleOriginalId = $role->id;
51
        } elseif ($role->status === Role::STATUS_DRAFT) {
52
            // Not using a constant here as this is legacy storage engine specific.
53
            // -1 means "Newly created role".
54
            $roleOriginalId = -1;
55
        } else {
56
            // Role::STATUS_DEFINED value is 0, which is the expected value for version column for this status.
57
            $roleOriginalId = Role::STATUS_DEFINED;
58
        }
59
60
        $query = $this->connection->createQueryBuilder();
61
        $query
62
            ->insert(self::ROLE_TABLE)
63
            ->values(
64
                [
65
                    'is_new' => $query->createPositionalParameter(0, ParameterType::INTEGER),
66
                    'name' => $query->createPositionalParameter(
67
                        $role->identifier,
68
                        ParameterType::STRING
69
                    ),
70
                    'value' => $query->createPositionalParameter(0, ParameterType::INTEGER),
71
                    // BC: "version" stores originalId when creating a draft from an existing role
72
                    'version' => $query->createPositionalParameter(
73
                        $roleOriginalId,
74
                        ParameterType::STRING
75
                    ),
76
                ]
77
            );
78
        $query->execute();
79
80
        if (!isset($role->id) || (int)$role->id < 1 || $role->status === Role::STATUS_DRAFT) {
81
            $role->id = (int)$this->connection->lastInsertId(self::ROLE_SEQ);
82
        }
83
84
        $role->originalId = $roleOriginalId;
85
86
        return $role;
87
    }
88
89
    private function getLoadRoleQueryBuilder(): QueryBuilder
90
    {
91
        $query = $this->connection->createQueryBuilder();
92
        $query
93
            ->select(
94
                'r.id AS ezrole_id',
95
                'r.name AS ezrole_name',
96
                'r.version AS ezrole_version',
97
                'p.id AS ezpolicy_id',
98
                'p.function_name AS ezpolicy_function_name',
99
                'p.module_name AS ezpolicy_module_name',
100
                'p.original_id AS ezpolicy_original_id',
101
                'l.identifier AS ezpolicy_limitation_identifier',
102
                'v.value AS ezpolicy_limitation_value_value'
103
            )
104
            ->from(self::ROLE_TABLE, 'r')
105
            ->leftJoin('r', self::POLICY_TABLE, 'p', 'p.role_id = r.id')
106
            ->leftJoin('p', self::POLICY_LIMITATION_TABLE, 'l', 'l.policy_id = p.id')
107
            ->leftJoin('l', self::POLICY_LIMITATION_VALUE_TABLE, 'v', 'v.limitation_id = l.id');
108
109
        return $query;
110
    }
111
112
    public function loadRole(int $roleId, int $status = Role::STATUS_DEFINED): array
113
    {
114
        $query = $this->getLoadRoleQueryBuilder();
115
        $query
116
            ->where(
117
                $query->expr()->eq(
118
                    'r.id',
119
                    $query->createPositionalParameter($roleId, ParameterType::INTEGER)
120
                )
121
            )
122
            ->andWhere(
123
                $this->buildRoleDraftQueryConstraint($status, $query)
124
            );
125
126
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
127
    }
128
129
    public function loadRoleByIdentifier(
130
        string $identifier,
131
        int $status = Role::STATUS_DEFINED
132
    ): array {
133
        $query = $this->getLoadRoleQueryBuilder();
134
        $query
135
            ->where(
136
                $query->expr()->eq(
137
                    'r.name',
138
                    $query->createPositionalParameter($identifier, ParameterType::STRING)
139
                )
140
            )
141
            ->andWhere(
142
                $this->buildRoleDraftQueryConstraint($status, $query)
143
            );
144
145
        $statement = $query->execute();
146
147
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
148
    }
149
150
    public function loadRoleDraftByRoleId(int $roleId): array
151
    {
152
        $query = $this->getLoadRoleQueryBuilder();
153
        // BC: "version" stores originalId when creating a draft from an existing role
154
        $query
155
            ->where(
156
                $query->expr()->eq(
157
                    'r.version',
158
                    $query->createPositionalParameter($roleId, ParameterType::STRING)
159
                )
160
            );
161
162
        $statement = $query->execute();
163
164
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
165
    }
166
167
    public function loadRoles(int $status = Role::STATUS_DEFINED): array
168
    {
169
        $query = $this->getLoadRoleQueryBuilder();
170
        $query->where(
171
            $this->buildRoleDraftQueryConstraint($status, $query)
172
        );
173
174
        $statement = $query->execute();
175
176
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
177
    }
178
179
    public function loadRolesForContentObjects(
180
        array $contentIds,
181
        int $status = Role::STATUS_DEFINED
182
    ): array {
183
        $query = $this->connection->createQueryBuilder();
184
        $expr = $query->expr();
185
        $query
186
            ->select(
187
                'ur.contentobject_id AS ezuser_role_contentobject_id',
188
                'r.id AS ezrole_id',
189
                'r.name AS ezrole_name',
190
                'r.version AS ezrole_version',
191
                'p.id AS ezpolicy_id',
192
                'p.function_name AS ezpolicy_function_name',
193
                'p.module_name AS ezpolicy_module_name',
194
                'p.original_id AS ezpolicy_original_id',
195
                'l.identifier AS ezpolicy_limitation_identifier',
196
                'v.value AS ezpolicy_limitation_value_value'
197
            )
198
            ->from(self::USER_ROLE_TABLE, 'urs')
199
            ->leftJoin(
200
                'urs',
201
                self::ROLE_TABLE,
202
                'r',
203
                // BC: for drafts the "version" column contains the original role ID
204
                $expr->eq(
205
                    $status === Role::STATUS_DEFINED ? 'r.id' : 'r.version',
206
                    'urs.role_id'
207
                )
208
            )
209
            ->leftJoin('r', self::USER_ROLE_TABLE, 'ur', 'ur.role_id = r.id')
210
            ->leftJoin('r', self::POLICY_TABLE, 'p', 'p.role_id = r.id')
211
            ->leftJoin('p', self::POLICY_LIMITATION_TABLE, 'l', 'l.policy_id = p.id')
212
            ->leftJoin('l', self::POLICY_LIMITATION_VALUE_TABLE, 'v', 'v.limitation_id = l.id')
213
            ->where(
214
                $expr->in(
215
                    'urs.contentobject_id',
216
                    $query->createPositionalParameter($contentIds, Connection::PARAM_INT_ARRAY)
217
                )
218
            );
219
220
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
221
    }
222
223
    public function loadRoleAssignment(int $roleAssignmentId): array
224
    {
225
        $query = $this->connection->createQueryBuilder();
226
        $query->select(
227
            'id',
228
            'contentobject_id',
229
            'limit_identifier',
230
            'limit_value',
231
            'role_id'
232
        )->from(
233
            self::USER_ROLE_TABLE
234
        )->where(
235
            $query->expr()->eq(
236
                'id',
237
                $query->createPositionalParameter($roleAssignmentId, ParameterType::INTEGER)
238
            )
239
        );
240
241
        $statement = $query->execute();
242
243
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
244
    }
245
246
    public function loadRoleAssignmentsByGroupId(int $groupId, bool $inherited = false): array
247
    {
248
        $query = $this->connection->createQueryBuilder();
249
        $query->select(
250
            'id',
251
            'contentobject_id',
252
            'limit_identifier',
253
            'limit_value',
254
            'role_id'
255
        )->from(
256
            self::USER_ROLE_TABLE
257
        );
258
259
        if ($inherited) {
260
            $groupIds = $this->fetchUserGroups($groupId);
261
            $groupIds[] = $groupId;
262
            $query->where(
263
                $query->expr()->in(
264
                    'contentobject_id',
265
                    $groupIds
266
                )
267
            );
268
        } else {
269
            $query->where(
270
                $query->expr()->eq(
271
                    'contentobject_id',
272
                    $query->createPositionalParameter($groupId, ParameterType::INTEGER)
273
                )
274
            );
275
        }
276
277
        $statement = $query->execute();
278
279
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
280
    }
281
282
    public function loadRoleAssignmentsByRoleId(int $roleId): array
283
    {
284
        $query = $this->connection->createQueryBuilder();
285
        $query->select(
286
            'id',
287
            'contentobject_id',
288
            'limit_identifier',
289
            'limit_value',
290
            'role_id'
291
        )->from(
292
            self::USER_ROLE_TABLE
293
        )->where(
294
            $query->expr()->eq(
295
                'role_id',
296
                $query->createPositionalParameter($roleId, ParameterType::INTEGER)
297
            )
298
        );
299
300
        $statement = $query->execute();
301
302
        return $statement->fetchAll(FetchMode::ASSOCIATIVE);
303
    }
304
305
    public function loadPoliciesByUserId(int $userId): array
306
    {
307
        $groupIds = $this->fetchUserGroups($userId);
308
        $groupIds[] = $userId;
309
310
        return $this->loadRolesForContentObjects($groupIds);
311
    }
312
313
    /**
314
     * Fetch all group IDs the user belongs to.
315
     *
316
     * This method will return Content ids of all ancestor Locations for the given $userId.
317
     * Note that not all of these might be used as user groups,
318
     * but we will need to check all of them.
319
     *
320
     * @param int $userId
321
     *
322
     * @return array
323
     */
324
    private function fetchUserGroups(int $userId): array
325
    {
326
        $nodeIDs = $this->getAncestorLocationIdsForUser($userId);
327
328
        if (empty($nodeIDs)) {
329
            return [];
330
        }
331
332
        $query = $this->connection->createQueryBuilder();
333
        $query
334
            ->select('c.id')
335
            ->from('ezcontentobject_tree', 't')
336
            ->innerJoin('t', 'ezcontentobject', 'c', 'c.id = t.contentobject_id')
337
            ->where(
338
                $query->expr()->in(
339
                    't.node_id',
340
                    $nodeIDs
341
                )
342
            );
343
344
        $statement = $query->execute();
345
346
        return $statement->fetchAll(FetchMode::COLUMN);
347
    }
348
349
    public function updateRole(RoleUpdateStruct $role): void
350
    {
351
        $query = $this->connection->createQueryBuilder();
352
        $query
353
            ->update(self::ROLE_TABLE)
354
            ->set(
355
                'name',
356
                $query->createPositionalParameter($role->identifier, ParameterType::STRING)
357
            )
358
            ->where(
359
                $query->expr()->eq(
360
                    'id',
361
                    $query->createPositionalParameter($role->id, ParameterType::INTEGER)
362
                )
363
            );
364
        $query->execute();
365
    }
366
367
    public function deleteRole(int $roleId, int $status = Role::STATUS_DEFINED): void
368
    {
369
        $query = $this->connection->createQueryBuilder();
370
        $expr = $query->expr();
371
        $query
372
            ->delete(self::ROLE_TABLE)
373
            ->where(
374
                $expr->eq(
375
                    'id',
376
                    $query->createPositionalParameter($roleId, ParameterType::INTEGER)
377
                )
378
            )
379
            ->andWhere(
380
                $this->buildRoleDraftQueryConstraint($status, $query, self::ROLE_TABLE)
381
            );
382
383
        if ($status !== Role::STATUS_DRAFT) {
384
            $this->deleteRoleAssignments($roleId);
385
        }
386
        $query->execute();
387
    }
388
389
    public function publishRoleDraft(int $roleDraftId, ?int $originalRoleId = null): void
390
    {
391
        $this->markRoleAsPublished($roleDraftId, $originalRoleId);
392
        $this->publishRolePolicies($roleDraftId, $originalRoleId);
393
    }
394
395
    public function addPolicy(int $roleId, Policy $policy): Policy
396
    {
397
        $query = $this->connection->createQueryBuilder();
398
        $query
399
            ->insert(self::POLICY_TABLE)
400
            ->values(
401
                [
402
                    'function_name' => $query->createPositionalParameter(
403
                        $policy->function,
404
                        ParameterType::STRING
405
                    ),
406
                    'module_name' => $query->createPositionalParameter(
407
                        $policy->module,
408
                        ParameterType::STRING
409
                    ),
410
                    'original_id' => $query->createPositionalParameter(
411
                        $policy->originalId ?? 0,
412
                        ParameterType::INTEGER
413
                    ),
414
                    'role_id' => $query->createPositionalParameter($roleId, ParameterType::INTEGER),
415
                ]
416
            );
417
        $query->execute();
418
419
        $policy->id = (int)$this->connection->lastInsertId(self::POLICY_SEQ);
420
        $policy->roleId = $roleId;
421
422
        // Handle the only valid non-array value "*" by not inserting
423
        // anything. Still has not been documented by eZ Systems. So we
424
        // assume this is the right way to handle it.
425
        if (is_array($policy->limitations)) {
426
            $this->addPolicyLimitations($policy->id, $policy->limitations);
427
        }
428
429
        return $policy;
430
    }
431
432
    public function addPolicyLimitations(int $policyId, array $limitations): void
433
    {
434
        foreach ($limitations as $identifier => $values) {
435
            $query = $this->connection->createQueryBuilder();
436
            $query
437
                ->insert(self::POLICY_LIMITATION_TABLE)
438
                ->values(
439
                    [
440
                        'identifier' => $query->createPositionalParameter(
441
                            $identifier,
442
                            ParameterType::STRING
443
                        ),
444
                        'policy_id' => $query->createPositionalParameter(
445
                            $policyId,
446
                            ParameterType::INTEGER
447
                        ),
448
                    ]
449
                );
450
            $query->execute();
451
452
            $limitationId = (int)$this->connection->lastInsertId(self::POLICY_LIMITATION_SEQ);
453
454
            foreach ($values as $value) {
455
                $query = $this->connection->createQueryBuilder();
456
                $query
457
                    ->insert(self::POLICY_LIMITATION_VALUE_TABLE)
458
                    ->values(
459
                        [
460
                            'value' => $query->createPositionalParameter(
461
                                $value,
462
                                ParameterType::STRING
463
                            ),
464
                            'limitation_id' => $query->createPositionalParameter(
465
                                $limitationId,
466
                                ParameterType::INTEGER
467
                            ),
468
                        ]
469
                    );
470
                $query->execute();
471
            }
472
        }
473
    }
474
475
    public function removePolicy(int $policyId): void
476
    {
477
        $this->removePolicyLimitations($policyId);
478
479
        $query = $this->connection->createQueryBuilder();
480
        $query
481
            ->delete(self::POLICY_TABLE)
482
            ->where(
483
                $query->expr()->eq(
484
                    'id',
485
                    $query->createPositionalParameter($policyId, ParameterType::INTEGER)
486
                )
487
            );
488
        $query->execute();
489
    }
490
491
    /**
492
     * @param int[] $limitationIds
493
     */
494
    private function deletePolicyLimitations(array $limitationIds): void
495
    {
496
        $query = $this->connection->createQueryBuilder();
497
        $query
498
            ->delete(self::POLICY_LIMITATION_TABLE)
499
            ->where(
500
                $query->expr()->in(
501
                    'id',
502
                    $query->createPositionalParameter(
503
                        $limitationIds,
504
                        Connection::PARAM_INT_ARRAY
505
                    )
506
                )
507
            );
508
        $query->execute();
509
    }
510
511
    /**
512
     * @param int[] $limitationValueIds
513
     */
514
    private function deletePolicyLimitationValues(array $limitationValueIds): void
515
    {
516
        $query = $this->connection->createQueryBuilder();
517
        $query
518
            ->delete(self::POLICY_LIMITATION_VALUE_TABLE)
519
            ->where(
520
                $query->expr()->in(
521
                    'id',
522
                    $query->createPositionalParameter(
523
                        $limitationValueIds,
524
                        Connection::PARAM_INT_ARRAY
525
                    )
526
                )
527
            );
528
        $query->execute();
529
    }
530
531
    private function loadPolicyLimitationValues(int $policyId): array
532
    {
533
        $query = $this->connection->createQueryBuilder();
534
        $query
535
            ->select(
536
                'l.id AS ezpolicy_limitation_id',
537
                'v.id AS ezpolicy_limitation_value_id'
538
            )
539
            ->from(self::POLICY_TABLE, 'p')
540
            ->leftJoin('p', self::POLICY_LIMITATION_TABLE, 'l', 'l.policy_id = p.id')
541
            ->leftJoin('l', self::POLICY_LIMITATION_VALUE_TABLE, 'v', 'v.limitation_id = l.id')
542
            ->where(
543
                $query->expr()->eq(
544
                    'p.id',
545
                    $query->createPositionalParameter($policyId, ParameterType::INTEGER)
546
                )
547
            );
548
549
        return $query->execute()->fetchAll(FetchMode::ASSOCIATIVE);
550
    }
551
552
    public function removePolicyLimitations(int $policyId): void
553
    {
554
        $limitationValues = $this->loadPolicyLimitationValues($policyId);
555
556
        $limitationIds = array_map(
557
            'intval',
558
            array_column($limitationValues, 'ezpolicy_limitation_id')
559
        );
560
        $limitationValueIds = array_map(
561
            'intval',
562
            array_column($limitationValues, 'ezpolicy_limitation_value_id')
563
        );
564
565
        if (!empty($limitationValueIds)) {
566
            $this->deletePolicyLimitationValues($limitationValueIds);
567
        }
568
569
        if (!empty($limitationIds)) {
570
            $this->deletePolicyLimitations($limitationIds);
571
        }
572
    }
573
574
    /**
575
     * Delete Role assignments to Users.
576
     */
577
    private function deleteRoleAssignments(int $roleId): void
578
    {
579
        $query = $this->connection->createQueryBuilder();
580
        $query
581
            ->delete(self::USER_ROLE_TABLE)
582
            ->where(
583
                $query->expr()->eq(
584
                    'role_id',
585
                    $query->createPositionalParameter($roleId, ParameterType::INTEGER)
586
                )
587
            );
588
        $query->execute();
589
    }
590
591
    /**
592
     * Load all Ancestor Location IDs of the given User Location.
593
     *
594
     * @param int $userId
595
     *
596
     * @return int[]
597
     */
598
    private function getAncestorLocationIdsForUser(int $userId): array
599
    {
600
        $query = $this->connection->createQueryBuilder();
601
        $query
602
            ->select('t.path_string')
603
            ->from('ezcontentobject_tree', 't')
604
            ->where(
605
                $query->expr()->eq(
606
                    't.contentobject_id',
607
                    $query->createPositionalParameter($userId, ParameterType::STRING)
608
                )
609
            );
610
611
        $paths = $query->execute()->fetchAll(FetchMode::COLUMN);
612
        $nodeIds = array_unique(
613
            array_reduce(
614
                array_map(
615
                    function ($pathString) {
616
                        return array_filter(explode('/', $pathString));
617
                    },
618
                    $paths
619
                ),
620
                'array_merge_recursive',
621
                []
622
            )
623
        );
624
625
        return array_map('intval', $nodeIds);
626
    }
627
628
    private function buildRoleDraftQueryConstraint(
629
        int $status,
630
        QueryBuilder $query,
631
        string $columnAlias = 'r'
632
    ): string {
633
        if ($status === Role::STATUS_DEFINED) {
634
            $draftCondition = $query->expr()->eq(
635
                "{$columnAlias}.version",
636
                $query->createPositionalParameter($status, ParameterType::INTEGER)
637
            );
638
        } else {
639
            // version stores original Role ID when Role is a draft...
640
            $draftCondition = $query->expr()->neq(
641
                "{$columnAlias}.version",
642
                $query->createPositionalParameter(Role::STATUS_DEFINED, ParameterType::INTEGER)
643
            );
644
        }
645
646
        return $draftCondition;
647
    }
648
649
    private function markRoleAsPublished(int $roleDraftId, ?int $originalRoleId): void
650
    {
651
        $query = $this->connection->createQueryBuilder();
652
        $query
653
            ->update(self::ROLE_TABLE)
654
            ->set(
655
                'version',
656
                $query->createPositionalParameter(Role::STATUS_DEFINED, ParameterType::INTEGER)
657
            );
658
        // Draft was created from an existing role, so published role must get the original ID.
659
        if ($originalRoleId !== null) {
660
            $query->set(
661
                'id',
662
                $query->createPositionalParameter($originalRoleId, ParameterType::INTEGER)
663
            );
664
        }
665
666
        $query->where(
667
            $query->expr()->eq(
668
                'id',
669
                $query->createPositionalParameter($roleDraftId, ParameterType::INTEGER)
670
            )
671
        );
672
        $query->execute();
673
    }
674
675
    private function publishRolePolicies(int $roleDraftId, ?int $originalRoleId): void
676
    {
677
        $policyQuery = $this->connection->createQueryBuilder();
678
        $policyQuery
679
            ->update(self::POLICY_TABLE)
680
            ->set(
681
                'original_id',
682
                $policyQuery->createPositionalParameter(0, ParameterType::INTEGER)
683
            );
684
        // Draft was created from an existing role, so published policies must get the original role ID.
685
        if ($originalRoleId !== null) {
686
            $policyQuery->set(
687
                'role_id',
688
                $policyQuery->createPositionalParameter($originalRoleId, ParameterType::INTEGER)
689
            );
690
        }
691
692
        $policyQuery->where(
693
            $policyQuery->expr()->eq(
694
                'role_id',
695
                $policyQuery->createPositionalParameter($roleDraftId, ParameterType::INTEGER)
696
            )
697
        );
698
        $policyQuery->execute();
699
    }
700
}
701