Completed
Pull Request — master (#14061)
by Leandro
12:19
created

DbManager::removeChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 6
cp 0
cc 1
eloc 6
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rbac;
9
10
use Yii;
11
use yii\caching\Cache;
12
use yii\db\Connection;
13
use yii\db\Query;
14
use yii\db\Expression;
15
use yii\base\InvalidCallException;
16
use yii\base\InvalidParamException;
17
use yii\di\Instance;
18
use yii\helpers\ArrayHelper;
19
20
/**
21
 * DbManager represents an authorization manager that stores authorization information in database.
22
 *
23
 * The database connection is specified by [[db]]. The database schema could be initialized by applying migration:
24
 *
25
 * ```
26
 * yii migrate --migrationPath=@yii/rbac/migrations/
27
 * ```
28
 *
29
 * If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
30
 *
31
 * You may change the names of the tables used to store the authorization and rule data by setting [[itemTable]],
32
 * [[itemChildTable]], [[assignmentTable]] and [[ruleTable]].
33
 *
34
 * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization).
35
 *
36
 * @author Qiang Xue <[email protected]>
37
 * @author Alexander Kochetov <[email protected]>
38
 * @since 2.0
39
 */
40
class DbManager extends BaseManager
41
{
42
    /**
43
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
44
     * After the DbManager object is created, if you want to change this property, you should only assign it
45
     * with a DB connection object.
46
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
47
     */
48
    public $db = 'db';
49
    /**
50
     * @var string the name of the table storing authorization items. Defaults to "auth_item".
51
     */
52
    public $itemTable = '{{%auth_item}}';
53
    /**
54
     * @var string the name of the table storing authorization item hierarchy. Defaults to "auth_item_child".
55
     */
56
    public $itemChildTable = '{{%auth_item_child}}';
57
    /**
58
     * @var string the name of the table storing authorization item assignments. Defaults to "auth_assignment".
59
     */
60
    public $assignmentTable = '{{%auth_assignment}}';
61
    /**
62
     * @var string the name of the table storing rules. Defaults to "auth_rule".
63
     */
64
    public $ruleTable = '{{%auth_rule}}';
65
    /**
66
     * @var Cache|array|string the cache used to improve RBAC performance. This can be one of the following:
67
     *
68
     * - an application component ID (e.g. `cache`)
69
     * - a configuration array
70
     * - a [[\yii\caching\Cache]] object
71
     *
72
     * When this is not set, it means caching is not enabled.
73
     *
74
     * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
75
     * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
76
     * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
77
     * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
78
     *
79
     * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
80
     * you have to manually call [[invalidateCache()]] to ensure data consistency.
81
     *
82
     * @since 2.0.3
83
     */
84
    public $cache;
85
    /**
86
     * @var string the key used to store RBAC data in cache
87
     * @see cache
88
     * @since 2.0.3
89
     */
90
    public $cacheKey = 'rbac';
91
92
    /**
93
     * @var Item[] all auth items (name => Item)
94
     */
95
    protected $items;
96
    /**
97
     * @var Rule[] all auth rules (name => Rule)
98
     */
99
    protected $rules;
100
    /**
101
     * @var array auth item parent-child relationships (childName => list of parents)
102
     */
103
    protected $parents;
104
105
    /**
106
     * @var array the map for user assigments
107
     */
108
    protected $assignments = [];
109
110
111
    /**
112
     * Initializes the application component.
113
     * This method overrides the parent implementation by establishing the database connection.
114
     */
115 92
    public function init()
116
    {
117 92
        parent::init();
118 92
        $this->db = Instance::ensure($this->db, Connection::className());
119 92
        if ($this->cache !== null) {
120 23
            $this->cache = Instance::ensure($this->cache, Cache::className());
121
        }
122 92
    }
123
124
    /**
125
     * @inheritdoc
126
     */
127 8
    public function checkAccess($userId, $permissionName, $params = [])
128
    {
129 8
        $assignments = ArrayHelper::getValue($this->assignments, $userId);
130
131 8
        if ($assignments === null) {
132 8
            $assignments = $this->getAssignments($userId);
133 8
            $this->assignments[$userId] = $assignments;
134
        }
135
136 8
        if ($this->hasNoAssignments($assignments)) {
137
            return false;
138
        }
139
140 8
        $this->loadFromCache();
141 8
        if ($this->items !== null) {
142 2
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
143
        } else {
144 6
            return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
145
        }
146
    }
147
148
    /**
149
     * Performs access check for the specified user based on the data loaded from cache.
150
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
151
     * @param string|int $user the user ID. This should can be either an integer or a string representing
152
     * the unique identifier of a user. See [[\yii\web\User::id]].
153
     * @param string $itemName the name of the operation that need access check
154
     * @param array $params name-value pairs that would be passed to rules associated
155
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
156
     * which holds the value of `$userId`.
157
     * @param Assignment[] $assignments the assignments to the specified user
158
     * @return bool whether the operations can be performed by the user.
159
     * @since 2.0.3
160
     */
161 2
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
162
    {
163 2
        if (!isset($this->items[$itemName])) {
164 1
            return false;
165
        }
166
167 2
        $item = $this->items[$itemName];
168
169 2
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
170
171 2
        if (!$this->executeRule($user, $item, $params)) {
172 2
            return false;
173
        }
174
175 2
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
176 2
            return true;
177
        }
178
179 1
        if (!empty($this->parents[$itemName])) {
180 1
            foreach ($this->parents[$itemName] as $parent) {
181 1
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
182 1
                    return true;
183
                }
184
            }
185
        }
186
187 1
        return false;
188
    }
189
190
    /**
191
     * Performs access check for the specified user.
192
     * This method is internally called by [[checkAccess()]].
193
     * @param string|int $user the user ID. This should can be either an integer or a string representing
194
     * the unique identifier of a user. See [[\yii\web\User::id]].
195
     * @param string $itemName the name of the operation that need access check
196
     * @param array $params name-value pairs that would be passed to rules associated
197
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
198
     * which holds the value of `$userId`.
199
     * @param Assignment[] $assignments the assignments to the specified user
200
     * @return bool whether the operations can be performed by the user.
201
     */
202 6
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
203
    {
204 6
        if (($item = $this->getItem($itemName)) === null) {
205 3
            return false;
206
        }
207
208 6
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
209
210 6
        if (!$this->executeRule($user, $item, $params)) {
211 6
            return false;
212
        }
213
214 6
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
215 6
            return true;
216
        }
217
218 3
        $query = new Query;
219 3
        $parents = $query->select(['parent'])
220 3
            ->from($this->itemChildTable)
221 3
            ->where(['child' => $itemName])
222 3
            ->column($this->db);
223 3
        foreach ($parents as $parent) {
224 3
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
225 3
                return true;
226
            }
227
        }
228
229 3
        return false;
230
    }
231
232
    /**
233
     * @inheritdoc
234
     */
235 35
    protected function getItem($name)
236
    {
237 35
        if (empty($name)) {
238 3
            return null;
239
        }
240
241 35
        if (!empty($this->items[$name])) {
242 1
            return $this->items[$name];
243
        }
244
245 34
        $row = (new Query)->from($this->itemTable)
246 34
            ->where(['name' => $name])
247 34
            ->one($this->db);
248
249 34
        if ($row === false) {
250 11
            return null;
251
        }
252
253 34
        return $this->populateItem($row);
254
    }
255
256
    /**
257
     * Returns a value indicating whether the database supports cascading update and delete.
258
     * The default implementation will return false for SQLite database and true for all other databases.
259
     * @return bool whether the database supports cascading update and delete.
260
     */
261 24
    protected function supportsCascadeUpdate()
262
    {
263 24
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269 80
    protected function addItem($item)
270
    {
271 80
        $time = time();
272 80
        if ($item->createdAt === null) {
273 80
            $item->createdAt = $time;
274
        }
275 80
        if ($item->updatedAt === null) {
276 80
            $item->updatedAt = $time;
277
        }
278 80
        $this->db->createCommand()
279 80
            ->insert($this->itemTable, [
280 80
                'name' => $item->name,
281 80
                'type' => $item->type,
282 80
                'description' => $item->description,
283 80
                'rule_name' => $item->ruleName,
284 80
                'data' => $item->data === null ? null : serialize($item->data),
285 80
                'created_at' => $item->createdAt,
286 80
                'updated_at' => $item->updatedAt,
287 80
            ])->execute();
288
289 80
        $this->invalidateCache();
290
291 80
        return true;
292
    }
293
294
    /**
295
     * @inheritdoc
296
     */
297 4
    protected function removeItem($item)
298
    {
299 4
        if (!$this->supportsCascadeUpdate()) {
300
            $this->db->createCommand()
301
                ->delete($this->itemChildTable, ['or', '[[parent]]=:name', '[[child]]=:name'], [':name' => $item->name])
302
                ->execute();
303
            $this->db->createCommand()
304
                ->delete($this->assignmentTable, ['item_name' => $item->name])
305
                ->execute();
306
        }
307
308 4
        $this->db->createCommand()
309 4
            ->delete($this->itemTable, ['name' => $item->name])
310 4
            ->execute();
311
312 4
        $this->invalidateCache();
313
314 4
        return true;
315
    }
316
317
    /**
318
     * @inheritdoc
319
     */
320 8
    protected function updateItem($name, $item)
321
    {
322 8
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
323
            $this->db->createCommand()
324
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
325
                ->execute();
326
            $this->db->createCommand()
327
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
328
                ->execute();
329
            $this->db->createCommand()
330
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
331
                ->execute();
332
        }
333
334 8
        $item->updatedAt = time();
335
336 8
        $this->db->createCommand()
337 8
            ->update($this->itemTable, [
338 8
                'name' => $item->name,
339 8
                'description' => $item->description,
340 8
                'rule_name' => $item->ruleName,
341 8
                'data' => $item->data === null ? null : serialize($item->data),
342 8
                'updated_at' => $item->updatedAt,
343
            ], [
344 8
                'name' => $name,
345 8
            ])->execute();
346
347 8
        $this->invalidateCache();
348
349 8
        return true;
350
    }
351
352
    /**
353
     * @inheritdoc
354
     */
355 80
    protected function addRule($rule)
356
    {
357 80
        $time = time();
358 80
        if ($rule->createdAt === null) {
359 80
            $rule->createdAt = $time;
360
        }
361 80
        if ($rule->updatedAt === null) {
362 80
            $rule->updatedAt = $time;
363
        }
364 80
        $this->db->createCommand()
365 80
            ->insert($this->ruleTable, [
366 80
                'name' => $rule->name,
367 80
                'data' => serialize($rule),
368 80
                'created_at' => $rule->createdAt,
369 80
                'updated_at' => $rule->updatedAt,
370 80
            ])->execute();
371
372 80
        $this->invalidateCache();
373
374 80
        return true;
375
    }
376
377
    /**
378
     * @inheritdoc
379
     */
380 4
    protected function updateRule($name, $rule)
381
    {
382 4
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
383
            $this->db->createCommand()
384
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
385
                ->execute();
386
        }
387
388 4
        $rule->updatedAt = time();
389
390 4
        $this->db->createCommand()
391 4
            ->update($this->ruleTable, [
392 4
                'name' => $rule->name,
393 4
                'data' => serialize($rule),
394 4
                'updated_at' => $rule->updatedAt,
395
            ], [
396 4
                'name' => $name,
397 4
            ])->execute();
398
399 4
        $this->invalidateCache();
400
401 4
        return true;
402
    }
403
404
    /**
405
     * @inheritdoc
406
     */
407 4
    protected function removeRule($rule)
408
    {
409 4
        if (!$this->supportsCascadeUpdate()) {
410
            $this->db->createCommand()
411
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
412
                ->execute();
413
        }
414
415 4
        $this->db->createCommand()
416 4
            ->delete($this->ruleTable, ['name' => $rule->name])
417 4
            ->execute();
418
419 4
        $this->invalidateCache();
420
421 4
        return true;
422
    }
423
424
    /**
425
     * @inheritdoc
426
     */
427 16
    protected function getItems($type)
428
    {
429 16
        $query = (new Query)
430 16
            ->from($this->itemTable)
431 16
            ->where(['type' => $type]);
432
433 16
        $items = [];
434 16
        foreach ($query->all($this->db) as $row) {
435 16
            $items[$row['name']] = $this->populateItem($row);
436
        }
437
438 16
        return $items;
439
    }
440
441
    /**
442
     * Populates an auth item with the data fetched from database
443
     * @param array $row the data from the auth item table
444
     * @return Item the populated auth item instance (either Role or Permission)
445
     */
446 76
    protected function populateItem($row)
447
    {
448 76
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
449
450 76
        if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) {
451 76
            $data = null;
452
        }
453
454 76
        return new $class([
455 76
            'name' => $row['name'],
456 76
            'type' => $row['type'],
457 76
            'description' => $row['description'],
458 76
            'ruleName' => $row['rule_name'],
459 76
            'data' => $data,
460 76
            'createdAt' => $row['created_at'],
461 76
            'updatedAt' => $row['updated_at'],
462
        ]);
463
    }
464
465
    /**
466
     * @inheritdoc
467
     */
468 8
    public function getRolesByUser($userId)
469
    {
470 8
        if (!isset($userId) || $userId === '') {
471
            return [];
472
        }
473
474 8
        $query = (new Query)->select('b.*')
475 8
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
476 8
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
477 8
            ->andWhere(['a.user_id' => (string) $userId])
478 8
            ->andWhere(['b.type' => Item::TYPE_ROLE]);
479
480 8
        $roles = $this->getDefaultRoleInstances();
481 8
        foreach ($query->all($this->db) as $row) {
482 8
            $roles[$row['name']] = $this->populateItem($row);
483
        }
484 8
        return $roles;
485
    }
486
487
    /**
488
     * @inheritdoc
489
     */
490 4
    public function getChildRoles($roleName)
491
    {
492 4
        $role = $this->getRole($roleName);
493
494 4
        if ($role === null) {
495
            throw new InvalidParamException("Role \"$roleName\" not found.");
496
        }
497
498 4
        $result = [];
499 4
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
500
501 4
        $roles = [$roleName => $role];
502
503 4
        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
504 4
            return array_key_exists($roleItem->name, $result);
505 4
        });
506
507 4
        return $roles;
508
    }
509
510
    /**
511
     * @inheritdoc
512
     */
513 4
    public function getPermissionsByRole($roleName)
514
    {
515 4
        $childrenList = $this->getChildrenList();
516 4
        $result = [];
517 4
        $this->getChildrenRecursive($roleName, $childrenList, $result);
518 4
        if (empty($result)) {
519
            return [];
520
        }
521 4
        $query = (new Query)->from($this->itemTable)->where([
522 4
            'type' => Item::TYPE_PERMISSION,
523 4
            'name' => array_keys($result),
524
        ]);
525 4
        $permissions = [];
526 4
        foreach ($query->all($this->db) as $row) {
527 4
            $permissions[$row['name']] = $this->populateItem($row);
528
        }
529 4
        return $permissions;
530
    }
531
532
    /**
533
     * @inheritdoc
534
     */
535 4
    public function getPermissionsByUser($userId)
536
    {
537 4
        if (empty($userId)) {
538
            return [];
539
        }
540
541 4
        $directPermission = $this->getDirectPermissionsByUser($userId);
542 4
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
543
544 4
        return array_merge($directPermission, $inheritedPermission);
545
    }
546
547
    /**
548
     * Returns all permissions that are directly assigned to user.
549
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
550
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
551
     * @since 2.0.7
552
     */
553 4
    protected function getDirectPermissionsByUser($userId)
554
    {
555 4
        $query = (new Query)->select('b.*')
556 4
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
557 4
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
558 4
            ->andWhere(['a.user_id' => (string) $userId])
559 4
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
560
561 4
        $permissions = [];
562 4
        foreach ($query->all($this->db) as $row) {
563 4
            $permissions[$row['name']] = $this->populateItem($row);
564
        }
565 4
        return $permissions;
566
    }
567
568
    /**
569
     * Returns all permissions that the user inherits from the roles assigned to him.
570
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
571
     * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
572
     * @since 2.0.7
573
     */
574 4
    protected function getInheritedPermissionsByUser($userId)
575
    {
576 4
        $query = (new Query)->select('item_name')
577 4
            ->from($this->assignmentTable)
578 4
            ->where(['user_id' => (string) $userId]);
579
580 4
        $childrenList = $this->getChildrenList();
581 4
        $result = [];
582 4
        foreach ($query->column($this->db) as $roleName) {
583 4
            $this->getChildrenRecursive($roleName, $childrenList, $result);
584
        }
585
586 4
        if (empty($result)) {
587
            return [];
588
        }
589
590 4
        $query = (new Query)->from($this->itemTable)->where([
591 4
            'type' => Item::TYPE_PERMISSION,
592 4
            'name' => array_keys($result),
593
        ]);
594 4
        $permissions = [];
595 4
        foreach ($query->all($this->db) as $row) {
596 4
            $permissions[$row['name']] = $this->populateItem($row);
597
        }
598 4
        return $permissions;
599
    }
600
601
    /**
602
     * Returns the children for every parent.
603
     * @return array the children list. Each array key is a parent item name,
604
     * and the corresponding array value is a list of child item names.
605
     */
606 12
    protected function getChildrenList()
607
    {
608 12
        $query = (new Query)->from($this->itemChildTable);
609 12
        $parents = [];
610 12
        foreach ($query->all($this->db) as $row) {
611 12
            $parents[$row['parent']][] = $row['child'];
612
        }
613 12
        return $parents;
614
    }
615
616
    /**
617
     * Recursively finds all children and grand children of the specified item.
618
     * @param string $name the name of the item whose children are to be looked for.
619
     * @param array $childrenList the child list built via [[getChildrenList()]]
620
     * @param array $result the children and grand children (in array keys)
621
     */
622 12
    protected function getChildrenRecursive($name, $childrenList, &$result)
623
    {
624 12
        if (isset($childrenList[$name])) {
625 12
            foreach ($childrenList[$name] as $child) {
626 12
                $result[$child] = true;
627 12
                $this->getChildrenRecursive($child, $childrenList, $result);
628
            }
629
        }
630 12
    }
631
632
    /**
633
     * @inheritdoc
634
     */
635 76
    public function getRule($name)
636
    {
637 76
        if ($this->rules !== null) {
638 2
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
639
        }
640
641 76
        $row = (new Query)->select(['data'])
642 76
            ->from($this->ruleTable)
643 76
            ->where(['name' => $name])
644 76
            ->one($this->db);
645 76
        if ($row === false) {
646 12
            return null;
647
        }
648 76
        $data = $row['data'];
649 76
        if (is_resource($data)) {
650
            $data = stream_get_contents($data);
651
        }
652 76
        return unserialize($data);
653
654
    }
655
656
    /**
657
     * @inheritdoc
658
     */
659 20
    public function getRules()
660
    {
661 20
        if ($this->rules !== null) {
662
            return $this->rules;
663
        }
664
665 20
        $query = (new Query)->from($this->ruleTable);
666
667 20
        $rules = [];
668 20
        foreach ($query->all($this->db) as $row) {
669 12
            $data = $row['data'];
670 12
            if (is_resource($data)) {
671
               $data = stream_get_contents($data);
672
            }
673 12
            $rules[$row['name']] = unserialize($data);
674
        }
675
676 20
        return $rules;
677
    }
678
679
    /**
680
     * @inheritdoc
681
     */
682
    public function getAssignment($roleName, $userId)
683
    {
684
        if (empty($userId)) {
685
            return null;
686
        }
687
688
        $row = (new Query)->from($this->assignmentTable)
689
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
690
            ->one($this->db);
691
692
        if ($row === false) {
693
            return null;
694
        }
695
696
        return new Assignment([
697
            'userId' => $row['user_id'],
698
            'roleName' => $row['item_name'],
699
            'createdAt' => $row['created_at'],
700
        ]);
701
    }
702
703
    /**
704
     * @inheritdoc
705
     */
706 12
    public function getAssignments($userId)
707
    {
708 12
        if (empty($userId)) {
709 4
            return [];
710
        }
711
712 12
        $query = (new Query)
713 12
            ->from($this->assignmentTable)
714 12
            ->where(['user_id' => (string) $userId]);
715
716 12
        $assignments = [];
717 12
        foreach ($query->all($this->db) as $row) {
718 12
            $assignments[$row['item_name']] = new Assignment([
719 12
                'userId' => $row['user_id'],
720 12
                'roleName' => $row['item_name'],
721 12
                'createdAt' => $row['created_at'],
722
            ]);
723
        }
724
725 12
        return $assignments;
726
    }
727
728
    /**
729
     * @inheritdoc
730
     * @since 2.0.8
731
     */
732 4
    public function canAddChild($parent, $child)
733
    {
734 4
        return !$this->detectLoop($parent, $child);
735
    }
736
737
    /**
738
     * @inheritdoc
739
     */
740 72
    public function addChild($parent, $child)
741
    {
742 72
        if ($parent->name === $child->name) {
743
            throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself.");
744
        }
745
746 72
        if ($parent instanceof Permission && $child instanceof Role) {
747
            throw new InvalidParamException('Cannot add a role as a child of a permission.');
748
        }
749
750 72
        if ($this->detectLoop($parent, $child)) {
751
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
752
        }
753
754 72
        $this->db->createCommand()
755 72
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
756 72
            ->execute();
757
758 72
        $this->invalidateCache();
759
760 72
        return true;
761
    }
762
763
    /**
764
     * @inheritdoc
765
     */
766
    public function removeChild($parent, $child)
767
    {
768
        $result = $this->db->createCommand()
769
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
770
            ->execute() > 0;
771
772
        $this->invalidateCache();
773
774
        return $result;
775
    }
776
777
    /**
778
     * @inheritdoc
779
     */
780
    public function removeChildren($parent)
781
    {
782
        $result = $this->db->createCommand()
783
            ->delete($this->itemChildTable, ['parent' => $parent->name])
784
            ->execute() > 0;
785
786
        $this->invalidateCache();
787
788
        return $result;
789
    }
790
791
    /**
792
     * @inheritdoc
793
     */
794
    public function hasChild($parent, $child)
795
    {
796
        return (new Query)
797
            ->from($this->itemChildTable)
798
            ->where(['parent' => $parent->name, 'child' => $child->name])
799
            ->one($this->db) !== false;
800
    }
801
802
    /**
803
     * @inheritdoc
804
     */
805 72
    public function getChildren($name)
806
    {
807 72
        $query = (new Query)
808 72
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
809 72
            ->from([$this->itemTable, $this->itemChildTable])
810 72
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
811
812 72
        $children = [];
813 72
        foreach ($query->all($this->db) as $row) {
814 72
            $children[$row['name']] = $this->populateItem($row);
815
        }
816
817 72
        return $children;
818
    }
819
820
    /**
821
     * Checks whether there is a loop in the authorization item hierarchy.
822
     * @param Item $parent the parent item
823
     * @param Item $child the child item to be added to the hierarchy
824
     * @return bool whether a loop exists
825
     */
826 72
    protected function detectLoop($parent, $child)
827
    {
828 72
        if ($child->name === $parent->name) {
829 4
            return true;
830
        }
831 72
        foreach ($this->getChildren($child->name) as $grandchild) {
832 68
            if ($this->detectLoop($parent, $grandchild)) {
833 4
                return true;
834
            }
835
        }
836 72
        return false;
837
    }
838
839
    /**
840
     * @inheritdoc
841
     */
842 72
    public function assign($role, $userId)
843
    {
844 72
        $assignment = new Assignment([
845 72
            'userId' => $userId,
846 72
            'roleName' => $role->name,
847 72
            'createdAt' => time(),
848
        ]);
849
850 72
        $this->db->createCommand()
851 72
            ->insert($this->assignmentTable, [
852 72
                'user_id' => $assignment->userId,
853 72
                'item_name' => $assignment->roleName,
854 72
                'created_at' => $assignment->createdAt,
855 72
            ])->execute();
856
857 72
        return $assignment;
858
    }
859
860
    /**
861
     * @inheritdoc
862
     */
863
    public function revoke($role, $userId)
864
    {
865
        if (empty($userId)) {
866
            return false;
867
        }
868
869
        return $this->db->createCommand()
870
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
871
            ->execute() > 0;
872
    }
873
874
    /**
875
     * @inheritdoc
876
     */
877
    public function revokeAll($userId)
878
    {
879
        if (empty($userId)) {
880
            return false;
881
        }
882
883
        return $this->db->createCommand()
884
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
885
            ->execute() > 0;
886
    }
887
888
    /**
889
     * @inheritdoc
890
     */
891 92
    public function removeAll()
892
    {
893 92
        $this->removeAllAssignments();
894 92
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
895 92
        $this->db->createCommand()->delete($this->itemTable)->execute();
896 92
        $this->db->createCommand()->delete($this->ruleTable)->execute();
897 92
        $this->invalidateCache();
898 92
    }
899
900
    /**
901
     * @inheritdoc
902
     */
903 4
    public function removeAllPermissions()
904
    {
905 4
        $this->removeAllItems(Item::TYPE_PERMISSION);
906 4
    }
907
908
    /**
909
     * @inheritdoc
910
     */
911 4
    public function removeAllRoles()
912
    {
913 4
        $this->removeAllItems(Item::TYPE_ROLE);
914 4
    }
915
916
    /**
917
     * Removes all auth items of the specified type.
918
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
919
     */
920 8
    protected function removeAllItems($type)
921
    {
922 8
        if (!$this->supportsCascadeUpdate()) {
923
            $names = (new Query)
924
                ->select(['name'])
925
                ->from($this->itemTable)
926
                ->where(['type' => $type])
927
                ->column($this->db);
928
            if (empty($names)) {
929
                return;
930
            }
931
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
932
            $this->db->createCommand()
933
                ->delete($this->itemChildTable, [$key => $names])
934
                ->execute();
935
            $this->db->createCommand()
936
                ->delete($this->assignmentTable, ['item_name' => $names])
937
                ->execute();
938
        }
939 8
        $this->db->createCommand()
940 8
            ->delete($this->itemTable, ['type' => $type])
941 8
            ->execute();
942
943 8
        $this->invalidateCache();
944 8
    }
945
946
    /**
947
     * @inheritdoc
948
     */
949 4
    public function removeAllRules()
950
    {
951 4
        if (!$this->supportsCascadeUpdate()) {
952
            $this->db->createCommand()
953
                ->update($this->itemTable, ['rule_name' => null])
954
                ->execute();
955
        }
956
957 4
        $this->db->createCommand()->delete($this->ruleTable)->execute();
958
959 4
        $this->invalidateCache();
960 4
    }
961
962
    /**
963
     * @inheritdoc
964
     */
965 92
    public function removeAllAssignments()
966
    {
967 92
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
968 92
    }
969
970 92
    public function invalidateCache()
971
    {
972 92
        $this->assignments = [];
973 92
        if ($this->cache !== null) {
974 23
            $this->cache->delete($this->cacheKey);
975 23
            $this->items = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<yii\rbac\Item>> of property $items.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
976 23
            $this->rules = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,object<yii\rbac\Rule>> of property $rules.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
977 23
            $this->parents = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $parents.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
978
        }
979 92
    }
980
981 8
    public function loadFromCache()
982
    {
983 8
        if ($this->items !== null || !$this->cache instanceof Cache) {
984 8
            return;
985
        }
986
987 2
        $data = $this->cache->get($this->cacheKey);
988 2
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
989
            list ($this->items, $this->rules, $this->parents) = $data;
990
            return;
991
        }
992
993 2
        $query = (new Query)->from($this->itemTable);
994 2
        $this->items = [];
995 2
        foreach ($query->all($this->db) as $row) {
996 2
            $this->items[$row['name']] = $this->populateItem($row);
997
        }
998
999 2
        $query = (new Query)->from($this->ruleTable);
1000 2
        $this->rules = [];
1001 2
        foreach ($query->all($this->db) as $row) {
1002 2
            $data = $row['data'];
1003 2
            if (is_resource($data)) {
1004
                $data = stream_get_contents($data);
1005
            }
1006 2
            $this->rules[$row['name']] = unserialize($data);
1007
        }
1008
1009 2
        $query = (new Query)->from($this->itemChildTable);
1010 2
        $this->parents = [];
1011 2
        foreach ($query->all($this->db) as $row) {
1012 1
            if (isset($this->items[$row['child']])) {
1013 1
                $this->parents[$row['child']][] = $row['parent'];
1014
            }
1015
        }
1016
1017 2
        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
1018 2
    }
1019
1020
    /**
1021
     * Returns all role assignment information for the specified role.
1022
     * @param string $roleName
1023
     * @return Assignment[] the assignments. An empty array will be
1024
     * returned if role is not assigned to any user.
1025
     * @since 2.0.7
1026
     */
1027 4
    public function getUserIdsByRole($roleName)
1028
    {
1029 4
        if (empty($roleName)) {
1030
            return [];
1031
        }
1032
1033 4
        return (new Query)->select('[[user_id]]')
1034 4
            ->from($this->assignmentTable)
1035 4
            ->where(['item_name' => $roleName])->column($this->db);
1036
    }
1037
}
1038