Passed
Pull Request — master (#19513)
by Alexander
09:28
created

DbManager::getUserRolesCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rbac;
9
10
use Yii;
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidCallException;
13
use yii\caching\CacheInterface;
14
use yii\db\Connection;
15
use yii\db\Expression;
16
use yii\db\Query;
17
use yii\di\Instance;
18
19
/**
20
 * DbManager represents an authorization manager that stores authorization information in database.
21
 *
22
 * The database connection is specified by [[db]]. The database schema could be initialized by applying migration:
23
 *
24
 * ```
25
 * yii migrate --migrationPath=@yii/rbac/migrations/
26
 * ```
27
 *
28
 * If you don't want to use migration and need SQL instead, files for all databases are in migrations directory.
29
 *
30
 * You may change the names of the tables used to store the authorization and rule data by setting [[itemTable]],
31
 * [[itemChildTable]], [[assignmentTable]] and [[ruleTable]].
32
 *
33
 * For more details and usage information on DbManager, see the [guide article on security authorization](guide:security-authorization).
34
 *
35
 * @author Qiang Xue <[email protected]>
36
 * @author Alexander Kochetov <[email protected]>
37
 * @since 2.0
38
 */
39
class DbManager extends BaseManager
40
{
41
    /**
42
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
43
     * After the DbManager object is created, if you want to change this property, you should only assign it
44
     * with a DB connection object.
45
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
46
     */
47
    public $db = 'db';
48
    /**
49
     * @var string the name of the table storing authorization items. Defaults to "auth_item".
50
     */
51
    public $itemTable = '{{%auth_item}}';
52
    /**
53
     * @var string the name of the table storing authorization item hierarchy. Defaults to "auth_item_child".
54
     */
55
    public $itemChildTable = '{{%auth_item_child}}';
56
    /**
57
     * @var string the name of the table storing authorization item assignments. Defaults to "auth_assignment".
58
     */
59
    public $assignmentTable = '{{%auth_assignment}}';
60
    /**
61
     * @var string the name of the table storing rules. Defaults to "auth_rule".
62
     */
63
    public $ruleTable = '{{%auth_rule}}';
64
    /**
65
     * @var CacheInterface|array|string|null the cache used to improve RBAC performance. This can be one of the following:
66
     *
67
     * - an application component ID (e.g. `cache`)
68
     * - a configuration array
69
     * - a [[\yii\caching\Cache]] object
70
     *
71
     * When this is not set, it means caching is not enabled.
72
     *
73
     * Note that by enabling RBAC cache, all auth items, rules and auth item parent-child relationships will
74
     * be cached and loaded into memory. This will improve the performance of RBAC permission check. However,
75
     * it does require extra memory and as a result may not be appropriate if your RBAC system contains too many
76
     * auth items. You should seek other RBAC implementations (e.g. RBAC based on Redis storage) in this case.
77
     *
78
     * Also note that if you modify RBAC items, rules or parent-child relationships from outside of this component,
79
     * you have to manually call [[invalidateCache()]] to ensure data consistency.
80
     *
81
     * @since 2.0.3
82
     */
83
    public $cache;
84
    /**
85
     * @var string the key used to store RBAC data in cache
86
     * @see cache
87
     * @since 2.0.3
88
     */
89
    public $cacheKey = 'rbac';
90
    /**
91
     * @var string the key used to store user RBAC roles in cache
92
     * @since 2.0.48
93
     */
94
    public $rolesCacheSuffix = 'roles';
95
96
    /**
97
     * @var Item[] all auth items (name => Item)
98
     */
99
    protected $items;
100
    /**
101
     * @var Rule[] all auth rules (name => Rule)
102
     */
103
    protected $rules;
104
    /**
105
     * @var array auth item parent-child relationships (childName => list of parents)
106
     */
107
    protected $parents;
108
    /**
109
     * @var array user assignments (user id => Assignment[])
110
     * @since `protected` since 2.0.38
111
     */
112
    protected $checkAccessAssignments = [];
113
114
115
    /**
116
     * Initializes the application component.
117
     * This method overrides the parent implementation by establishing the database connection.
118
     */
119 245
    public function init()
120
    {
121 245
        parent::init();
122 245
        $this->db = Instance::ensure($this->db, Connection::className());
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

122
        $this->db = Instance::ensure($this->db, /** @scrutinizer ignore-deprecated */ Connection::className());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
123 245
        if ($this->cache !== null) {
124 98
            $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
125
        }
126 245
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 30
    public function checkAccess($userId, $permissionName, $params = [])
132
    {
133 30
        if (isset($this->checkAccessAssignments[(string) $userId])) {
134 30
            $assignments = $this->checkAccessAssignments[(string) $userId];
135
        } else {
136 30
            $assignments = $this->getAssignments($userId);
137 30
            $this->checkAccessAssignments[(string) $userId] = $assignments;
138
        }
139
140 30
        if ($this->hasNoAssignments($assignments)) {
141
            return false;
142
        }
143
144 30
        $this->loadFromCache();
145 30
        if ($this->items !== null) {
146 15
            return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
147
        }
148
149 15
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
150
    }
151
152
    /**
153
     * Performs access check for the specified user based on the data loaded from cache.
154
     * This method is internally called by [[checkAccess()]] when [[cache]] is enabled.
155
     * @param string|int $user the user ID. This should can be either an integer or a string representing
156
     * the unique identifier of a user. See [[\yii\web\User::id]].
157
     * @param string $itemName the name of the operation that need access check
158
     * @param array $params name-value pairs that would be passed to rules associated
159
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
160
     * which holds the value of `$userId`.
161
     * @param Assignment[] $assignments the assignments to the specified user
162
     * @return bool whether the operations can be performed by the user.
163
     * @since 2.0.3
164
     */
165 15
    protected function checkAccessFromCache($user, $itemName, $params, $assignments)
166
    {
167 15
        if (!isset($this->items[$itemName])) {
168 2
            return false;
169
        }
170
171 15
        $item = $this->items[$itemName];
172
173 15
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
174
175 15
        if (!$this->executeRule($user, $item, $params)) {
176 10
            return false;
177
        }
178
179 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
180 11
            return true;
181
        }
182
183 11
        if (!empty($this->parents[$itemName])) {
184 7
            foreach ($this->parents[$itemName] as $parent) {
185 7
                if ($this->checkAccessFromCache($user, $parent, $params, $assignments)) {
186 7
                    return true;
187
                }
188
            }
189
        }
190
191 11
        return false;
192
    }
193
194
    /**
195
     * Performs access check for the specified user.
196
     * This method is internally called by [[checkAccess()]].
197
     * @param string|int $user the user ID. This should can be either an integer or a string representing
198
     * the unique identifier of a user. See [[\yii\web\User::id]].
199
     * @param string $itemName the name of the operation that need access check
200
     * @param array $params name-value pairs that would be passed to rules associated
201
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
202
     * which holds the value of `$userId`.
203
     * @param Assignment[] $assignments the assignments to the specified user
204
     * @return bool whether the operations can be performed by the user.
205
     */
206 15
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
207
    {
208 15
        if (($item = $this->getItem($itemName)) === null) {
209 3
            return false;
210
        }
211
212 15
        Yii::debug($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
213
214 15
        if (!$this->executeRule($user, $item, $params)) {
215 15
            return false;
216
        }
217
218 15
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
219 9
            return true;
220
        }
221
222 9
        $query = new Query();
223 9
        $parents = $query->select(['parent'])
224 9
            ->from($this->itemChildTable)
225 9
            ->where(['child' => $itemName])
226 9
            ->column($this->db);
227 9
        foreach ($parents as $parent) {
228 3
            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
229 3
                return true;
230
            }
231
        }
232
233 9
        return false;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 84
    protected function getItem($name)
240
    {
241 84
        if (empty($name)) {
242 3
            return null;
243
        }
244
245 84
        if (!empty($this->items[$name])) {
246 9
            return $this->items[$name];
247
        }
248
249 75
        $row = (new Query())->from($this->itemTable)
250 75
            ->where(['name' => $name])
251 75
            ->one($this->db);
252
253 75
        if ($row === false) {
254 13
            return null;
255
        }
256
257 75
        return $this->populateItem($row);
258
    }
259
260
    /**
261
     * Returns a value indicating whether the database supports cascading update and delete.
262
     * The default implementation will return false for SQLite database and true for all other databases.
263
     * @return bool whether the database supports cascading update and delete.
264
     */
265 35
    protected function supportsCascadeUpdate()
266
    {
267 35
        return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 0;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273 220
    protected function addItem($item)
274
    {
275 220
        $time = time();
276 220
        if ($item->createdAt === null) {
277 220
            $item->createdAt = $time;
278
        }
279 220
        if ($item->updatedAt === null) {
280 220
            $item->updatedAt = $time;
281
        }
282 220
        $this->db->createCommand()
283 220
            ->insert($this->itemTable, [
284 220
                'name' => $item->name,
285 220
                'type' => $item->type,
286 220
                'description' => $item->description,
287 220
                'rule_name' => $item->ruleName,
288 220
                'data' => $item->data === null ? null : serialize($item->data),
289 220
                'created_at' => $item->createdAt,
290 220
                'updated_at' => $item->updatedAt,
291 220
            ])->execute();
292
293 220
        $this->invalidateCache();
294
295 220
        return true;
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 5
    protected function removeItem($item)
302
    {
303 5
        if (!$this->supportsCascadeUpdate()) {
304 1
            $this->db->createCommand()
305 1
                ->delete($this->itemChildTable, ['or', '[[parent]]=:parent', '[[child]]=:child'], [':parent' => $item->name, ':child' => $item->name])
306 1
                ->execute();
307 1
            $this->db->createCommand()
308 1
                ->delete($this->assignmentTable, ['item_name' => $item->name])
309 1
                ->execute();
310
        }
311
312 5
        $this->db->createCommand()
313 5
            ->delete($this->itemTable, ['name' => $item->name])
314 5
            ->execute();
315
316 5
        $this->invalidateCache();
317
318 5
        return true;
319
    }
320
321
    /**
322
     * {@inheritdoc}
323
     */
324 15
    protected function updateItem($name, $item)
325
    {
326 15
        if ($item->name !== $name && !$this->supportsCascadeUpdate()) {
327 3
            $this->db->createCommand()
328 3
                ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name])
329 3
                ->execute();
330 3
            $this->db->createCommand()
331 3
                ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name])
332 3
                ->execute();
333 3
            $this->db->createCommand()
334 3
                ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name])
335 3
                ->execute();
336
        }
337
338 15
        $item->updatedAt = time();
339
340 15
        $this->db->createCommand()
341 15
            ->update($this->itemTable, [
342 15
                'name' => $item->name,
343 15
                'description' => $item->description,
344 15
                'rule_name' => $item->ruleName,
345 15
                'data' => $item->data === null ? null : serialize($item->data),
346 15
                'updated_at' => $item->updatedAt,
347
            ], [
348 15
                'name' => $name,
349 15
            ])->execute();
350
351 15
        $this->invalidateCache();
352
353 15
        return true;
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 130
    protected function addRule($rule)
360
    {
361 130
        $time = time();
362 130
        if ($rule->createdAt === null) {
363 130
            $rule->createdAt = $time;
364
        }
365 130
        if ($rule->updatedAt === null) {
366 130
            $rule->updatedAt = $time;
367
        }
368 130
        $this->db->createCommand()
369 130
            ->insert($this->ruleTable, [
370 130
                'name' => $rule->name,
371 130
                'data' => serialize($rule),
372 130
                'created_at' => $rule->createdAt,
373 130
                'updated_at' => $rule->updatedAt,
374 130
            ])->execute();
375
376 130
        $this->invalidateCache();
377
378 130
        return true;
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384 5
    protected function updateRule($name, $rule)
385
    {
386 5
        if ($rule->name !== $name && !$this->supportsCascadeUpdate()) {
387 1
            $this->db->createCommand()
388 1
                ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name])
389 1
                ->execute();
390
        }
391
392 5
        $rule->updatedAt = time();
393
394 5
        $this->db->createCommand()
395 5
            ->update($this->ruleTable, [
396 5
                'name' => $rule->name,
397 5
                'data' => serialize($rule),
398 5
                'updated_at' => $rule->updatedAt,
399
            ], [
400 5
                'name' => $name,
401 5
            ])->execute();
402
403 5
        $this->invalidateCache();
404
405 5
        return true;
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     */
411 5
    protected function removeRule($rule)
412
    {
413 5
        if (!$this->supportsCascadeUpdate()) {
414 1
            $this->db->createCommand()
415 1
                ->update($this->itemTable, ['rule_name' => null], ['rule_name' => $rule->name])
416 1
                ->execute();
417
        }
418
419 5
        $this->db->createCommand()
420 5
            ->delete($this->ruleTable, ['name' => $rule->name])
421 5
            ->execute();
422
423 5
        $this->invalidateCache();
424
425 5
        return true;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 20
    protected function getItems($type)
432
    {
433 20
        $query = (new Query())
434 20
            ->from($this->itemTable)
435 20
            ->where(['type' => $type]);
436
437 20
        $items = [];
438 20
        foreach ($query->all($this->db) as $row) {
439 20
            $items[$row['name']] = $this->populateItem($row);
440
        }
441
442 20
        return $items;
443
    }
444
445
    /**
446
     * Populates an auth item with the data fetched from database.
447
     * @param array $row the data from the auth item table
448
     * @return Item the populated auth item instance (either Role or Permission)
449
     */
450 160
    protected function populateItem($row)
451
    {
452 160
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

452
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : /** @scrutinizer ignore-deprecated */ Role::className();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
453
454 160
        if (!isset($row['data']) || ($data = @unserialize(is_resource($row['data']) ? stream_get_contents($row['data']) : $row['data'])) === false) {
455 160
            $data = null;
456
        }
457
458 160
        return new $class([
459 160
            'name' => $row['name'],
460 160
            'type' => $row['type'],
461 160
            'description' => $row['description'],
462 160
            'ruleName' => $row['rule_name'] ?: null,
463 160
            'data' => $data,
464 160
            'createdAt' => $row['created_at'],
465 160
            'updatedAt' => $row['updated_at'],
466
        ]);
467
    }
468
469
    /**
470
     * {@inheritdoc}
471
     * The roles returned by this method include the roles assigned via [[$defaultRoles]].
472
     */
473 25
    public function getRolesByUser($userId)
474
    {
475 25
        if ($this->isEmptyUserId($userId)) {
476 5
            return [];
477
        }
478
479 20
        if ($this->cache !== null) {
480 8
            $data = $this->cache->get($this->getUserRolesCacheKey());
481
482 8
            if ($data !== false) {
483 2
                return $data;
484
            }
485
        }
486
487 20
        $query = (new Query())->select('b.*')
488 20
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
489 20
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
490 20
            ->andWhere(['a.user_id' => (string) $userId])
491 20
            ->andWhere(['b.type' => Item::TYPE_ROLE]);
492
493 20
        $roles = $this->getDefaultRoleInstances();
494 20
        foreach ($query->all($this->db) as $row) {
495 20
            $roles[$row['name']] = $this->populateItem($row);
496
        }
497
498 20
        if ($this->cache !== null) {
499 8
            $this->cache->set($this->getUserRolesCacheKey(), $roles);
500
        }
501
502 20
        return $roles;
503
    }
504
505
    /**
506
     * {@inheritdoc}
507
     */
508 5
    public function getChildRoles($roleName)
509
    {
510 5
        $role = $this->getRole($roleName);
511
512 5
        if ($role === null) {
513
            throw new InvalidArgumentException("Role \"$roleName\" not found.");
514
        }
515
516 5
        $result = [];
517 5
        $this->getChildrenRecursive($roleName, $this->getChildrenList(), $result);
518
519 5
        $roles = [$roleName => $role];
520
521 5
        $roles += array_filter($this->getRoles(), function (Role $roleItem) use ($result) {
522 5
            return array_key_exists($roleItem->name, $result);
523 5
        });
524
525 5
        return $roles;
526
    }
527
528
    /**
529
     * {@inheritdoc}
530
     */
531 5
    public function getPermissionsByRole($roleName)
532
    {
533 5
        $childrenList = $this->getChildrenList();
534 5
        $result = [];
535 5
        $this->getChildrenRecursive($roleName, $childrenList, $result);
536 5
        if (empty($result)) {
537
            return [];
538
        }
539 5
        $query = (new Query())->from($this->itemTable)->where([
540 5
            'type' => Item::TYPE_PERMISSION,
541 5
            'name' => array_keys($result),
542
        ]);
543 5
        $permissions = [];
544 5
        foreach ($query->all($this->db) as $row) {
545 5
            $permissions[$row['name']] = $this->populateItem($row);
546
        }
547
548 5
        return $permissions;
549
    }
550
551
    /**
552
     * {@inheritdoc}
553
     */
554 20
    public function getPermissionsByUser($userId)
555
    {
556 20
        if ($this->isEmptyUserId($userId)) {
557 5
            return [];
558
        }
559
560 15
        $directPermission = $this->getDirectPermissionsByUser($userId);
561 15
        $inheritedPermission = $this->getInheritedPermissionsByUser($userId);
562
563 15
        return array_merge($directPermission, $inheritedPermission);
564
    }
565
566
    /**
567
     * Returns all permissions that are directly assigned to user.
568
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
569
     * @return Permission[] all direct permissions that the user has. The array is indexed by the permission names.
570
     * @since 2.0.7
571
     */
572 15
    protected function getDirectPermissionsByUser($userId)
573
    {
574 15
        $query = (new Query())->select('b.*')
575 15
            ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable])
576 15
            ->where('{{a}}.[[item_name]]={{b}}.[[name]]')
577 15
            ->andWhere(['a.user_id' => (string) $userId])
578 15
            ->andWhere(['b.type' => Item::TYPE_PERMISSION]);
579
580 15
        $permissions = [];
581 15
        foreach ($query->all($this->db) as $row) {
582 15
            $permissions[$row['name']] = $this->populateItem($row);
583
        }
584
585 15
        return $permissions;
586
    }
587
588
    /**
589
     * Returns all permissions that the user inherits from the roles assigned to him.
590
     * @param string|int $userId the user ID (see [[\yii\web\User::id]])
591
     * @return Permission[] all inherited permissions that the user has. The array is indexed by the permission names.
592
     * @since 2.0.7
593
     */
594 15
    protected function getInheritedPermissionsByUser($userId)
595
    {
596 15
        $query = (new Query())->select('item_name')
597 15
            ->from($this->assignmentTable)
598 15
            ->where(['user_id' => (string) $userId]);
599
600 15
        $childrenList = $this->getChildrenList();
601 15
        $result = [];
602 15
        foreach ($query->column($this->db) as $roleName) {
603 15
            $this->getChildrenRecursive($roleName, $childrenList, $result);
604
        }
605
606 15
        if (empty($result)) {
607 10
            return [];
608
        }
609
610 5
        $query = (new Query())->from($this->itemTable)->where([
611 5
            'type' => Item::TYPE_PERMISSION,
612 5
            'name' => array_keys($result),
613
        ]);
614 5
        $permissions = [];
615 5
        foreach ($query->all($this->db) as $row) {
616 5
            $permissions[$row['name']] = $this->populateItem($row);
617
        }
618
619 5
        return $permissions;
620
    }
621
622
    /**
623
     * Returns the children for every parent.
624
     * @return array the children list. Each array key is a parent item name,
625
     * and the corresponding array value is a list of child item names.
626
     */
627 25
    protected function getChildrenList()
628
    {
629 25
        $query = (new Query())->from($this->itemChildTable);
630 25
        $parents = [];
631 25
        foreach ($query->all($this->db) as $row) {
632 15
            $parents[$row['parent']][] = $row['child'];
633
        }
634
635 25
        return $parents;
636
    }
637
638
    /**
639
     * Recursively finds all children and grand children of the specified item.
640
     * @param string $name the name of the item whose children are to be looked for.
641
     * @param array $childrenList the child list built via [[getChildrenList()]]
642
     * @param array $result the children and grand children (in array keys)
643
     */
644 25
    protected function getChildrenRecursive($name, $childrenList, &$result)
645
    {
646 25
        if (isset($childrenList[$name])) {
647 15
            foreach ($childrenList[$name] as $child) {
648 15
                $result[$child] = true;
649 15
                $this->getChildrenRecursive($child, $childrenList, $result);
650
            }
651
        }
652 25
    }
653
654
    /**
655
     * {@inheritdoc}
656
     */
657 125
    public function getRule($name)
658
    {
659 125
        if ($this->rules !== null) {
660 10
            return isset($this->rules[$name]) ? $this->rules[$name] : null;
661
        }
662
663 125
        $row = (new Query())->select(['data'])
664 125
            ->from($this->ruleTable)
665 125
            ->where(['name' => $name])
666 125
            ->one($this->db);
667 125
        if ($row === false) {
668 20
            return null;
669
        }
670 125
        $data = $row['data'];
671 125
        if (is_resource($data)) {
672 50
            $data = stream_get_contents($data);
673
        }
674 125
        if (!$data) {
675
            return null;
676
        }
677 125
        return unserialize($data);
678
    }
679
680
    /**
681
     * {@inheritdoc}
682
     */
683 25
    public function getRules()
684
    {
685 25
        if ($this->rules !== null) {
686
            return $this->rules;
687
        }
688
689 25
        $query = (new Query())->from($this->ruleTable);
690
691 25
        $rules = [];
692 25
        foreach ($query->all($this->db) as $row) {
693 15
            $data = $row['data'];
694 15
            if (is_resource($data)) {
695 6
                $data = stream_get_contents($data);
696
            }
697 15
            if ($data) {
698 15
                $rules[$row['name']] = unserialize($data);
699
            }
700
        }
701
702 25
        return $rules;
703
    }
704
705
    /**
706
     * {@inheritdoc}
707
     */
708 15
    public function getAssignment($roleName, $userId)
709
    {
710 15
        if ($this->isEmptyUserId($userId)) {
711 5
            return null;
712
        }
713
714 10
        $row = (new Query())->from($this->assignmentTable)
715 10
            ->where(['user_id' => (string) $userId, 'item_name' => $roleName])
716 10
            ->one($this->db);
717
718 10
        if ($row === false) {
719
            return null;
720
        }
721
722 10
        return new Assignment([
723 10
            'userId' => $row['user_id'],
724 10
            'roleName' => $row['item_name'],
725 10
            'createdAt' => $row['created_at'],
726
        ]);
727
    }
728
729
    /**
730
     * {@inheritdoc}
731
     */
732 50
    public function getAssignments($userId)
733
    {
734 50
        if ($this->isEmptyUserId($userId)) {
735 5
            return [];
736
        }
737
738 45
        $query = (new Query())
739 45
            ->from($this->assignmentTable)
740 45
            ->where(['user_id' => (string) $userId]);
741
742 45
        $assignments = [];
743 45
        foreach ($query->all($this->db) as $row) {
744 35
            $assignments[$row['item_name']] = new Assignment([
745 35
                'userId' => $row['user_id'],
746 35
                'roleName' => $row['item_name'],
747 35
                'createdAt' => $row['created_at'],
748
            ]);
749
        }
750
751 45
        return $assignments;
752
    }
753
754
    /**
755
     * {@inheritdoc}
756
     * @since 2.0.8
757
     */
758 5
    public function canAddChild($parent, $child)
759
    {
760 5
        return !$this->detectLoop($parent, $child);
761
    }
762
763
    /**
764
     * {@inheritdoc}
765
     */
766 105
    public function addChild($parent, $child)
767
    {
768 105
        if ($parent->name === $child->name) {
769
            throw new InvalidArgumentException("Cannot add '{$parent->name}' as a child of itself.");
770
        }
771
772 105
        if ($parent instanceof Permission && $child instanceof Role) {
773
            throw new InvalidArgumentException('Cannot add a role as a child of a permission.');
774
        }
775
776 105
        if ($this->detectLoop($parent, $child)) {
777
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
778
        }
779
780 105
        $this->db->createCommand()
781 105
            ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
782 105
            ->execute();
783
784 105
        $this->invalidateCache();
785
786 105
        return true;
787
    }
788
789
    /**
790
     * {@inheritdoc}
791
     */
792
    public function removeChild($parent, $child)
793
    {
794
        $result = $this->db->createCommand()
795
            ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name])
796
            ->execute() > 0;
797
798
        $this->invalidateCache();
799
800
        return $result;
801
    }
802
803
    /**
804
     * {@inheritdoc}
805
     */
806
    public function removeChildren($parent)
807
    {
808
        $result = $this->db->createCommand()
809
            ->delete($this->itemChildTable, ['parent' => $parent->name])
810
            ->execute() > 0;
811
812
        $this->invalidateCache();
813
814
        return $result;
815
    }
816
817
    /**
818
     * {@inheritdoc}
819
     */
820
    public function hasChild($parent, $child)
821
    {
822
        return (new Query())
823
            ->from($this->itemChildTable)
824
            ->where(['parent' => $parent->name, 'child' => $child->name])
825
            ->one($this->db) !== false;
826
    }
827
828
    /**
829
     * {@inheritdoc}
830
     */
831 105
    public function getChildren($name)
832
    {
833 105
        $query = (new Query())
834 105
            ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at'])
835 105
            ->from([$this->itemTable, $this->itemChildTable])
836 105
            ->where(['parent' => $name, 'name' => new Expression('[[child]]')]);
837
838 105
        $children = [];
839 105
        foreach ($query->all($this->db) as $row) {
840 105
            $children[$row['name']] = $this->populateItem($row);
841
        }
842
843 105
        return $children;
844
    }
845
846
    /**
847
     * Checks whether there is a loop in the authorization item hierarchy.
848
     * @param Item $parent the parent item
849
     * @param Item $child the child item to be added to the hierarchy
850
     * @return bool whether a loop exists
851
     */
852 105
    protected function detectLoop($parent, $child)
853
    {
854 105
        if ($child->name === $parent->name) {
855 5
            return true;
856
        }
857 105
        foreach ($this->getChildren($child->name) as $grandchild) {
858 100
            if ($this->detectLoop($parent, $grandchild)) {
859 5
                return true;
860
            }
861
        }
862
863 105
        return false;
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869 210
    public function assign($role, $userId)
870
    {
871 210
        $assignment = new Assignment([
872 210
            'userId' => $userId,
873 210
            'roleName' => $role->name,
874 210
            'createdAt' => time(),
875
        ]);
876
877 210
        $this->db->createCommand()
878 210
            ->insert($this->assignmentTable, [
879 210
                'user_id' => $assignment->userId,
880 210
                'item_name' => $assignment->roleName,
881 210
                'created_at' => $assignment->createdAt,
882 210
            ])->execute();
883
884 210
        unset($this->checkAccessAssignments[(string) $userId]);
885 210
        return $assignment;
886
    }
887
888
    /**
889
     * {@inheritdoc}
890
     */
891 30
    public function revoke($role, $userId)
892
    {
893 30
        if ($this->isEmptyUserId($userId)) {
894 5
            return false;
895
        }
896
897 25
        unset($this->checkAccessAssignments[(string) $userId]);
898 25
        return $this->db->createCommand()
899 25
            ->delete($this->assignmentTable, ['user_id' => (string) $userId, 'item_name' => $role->name])
900 25
            ->execute() > 0;
901
    }
902
903
    /**
904
     * {@inheritdoc}
905
     */
906 20
    public function revokeAll($userId)
907
    {
908 20
        if ($this->isEmptyUserId($userId)) {
909 5
            return false;
910
        }
911
912 15
        unset($this->checkAccessAssignments[(string) $userId]);
913 15
        return $this->db->createCommand()
914 15
            ->delete($this->assignmentTable, ['user_id' => (string) $userId])
915 15
            ->execute() > 0;
916
    }
917
918
    /**
919
     * {@inheritdoc}
920
     */
921 245
    public function removeAll()
922
    {
923 245
        $this->removeAllAssignments();
924 245
        $this->db->createCommand()->delete($this->itemChildTable)->execute();
925 245
        $this->db->createCommand()->delete($this->itemTable)->execute();
926 245
        $this->db->createCommand()->delete($this->ruleTable)->execute();
927 245
        $this->invalidateCache();
928 245
    }
929
930
    /**
931
     * {@inheritdoc}
932
     */
933 5
    public function removeAllPermissions()
934
    {
935 5
        $this->removeAllItems(Item::TYPE_PERMISSION);
936 5
    }
937
938
    /**
939
     * {@inheritdoc}
940
     */
941 5
    public function removeAllRoles()
942
    {
943 5
        $this->removeAllItems(Item::TYPE_ROLE);
944 5
    }
945
946
    /**
947
     * Removes all auth items of the specified type.
948
     * @param int $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
949
     */
950 10
    protected function removeAllItems($type)
951
    {
952 10
        if (!$this->supportsCascadeUpdate()) {
953 2
            $names = (new Query())
954 2
                ->select(['name'])
955 2
                ->from($this->itemTable)
956 2
                ->where(['type' => $type])
957 2
                ->column($this->db);
958 2
            if (empty($names)) {
959
                return;
960
            }
961 2
            $key = $type == Item::TYPE_PERMISSION ? 'child' : 'parent';
962 2
            $this->db->createCommand()
963 2
                ->delete($this->itemChildTable, [$key => $names])
964 2
                ->execute();
965 2
            $this->db->createCommand()
966 2
                ->delete($this->assignmentTable, ['item_name' => $names])
967 2
                ->execute();
968
        }
969 10
        $this->db->createCommand()
970 10
            ->delete($this->itemTable, ['type' => $type])
971 10
            ->execute();
972
973 10
        $this->invalidateCache();
974 10
    }
975
976
    /**
977
     * {@inheritdoc}
978
     */
979 5
    public function removeAllRules()
980
    {
981 5
        if (!$this->supportsCascadeUpdate()) {
982 1
            $this->db->createCommand()
983 1
                ->update($this->itemTable, ['rule_name' => null])
984 1
                ->execute();
985
        }
986
987 5
        $this->db->createCommand()->delete($this->ruleTable)->execute();
988
989 5
        $this->invalidateCache();
990 5
    }
991
992
    /**
993
     * {@inheritdoc}
994
     */
995 245
    public function removeAllAssignments()
996
    {
997 245
        $this->checkAccessAssignments = [];
998 245
        $this->db->createCommand()->delete($this->assignmentTable)->execute();
999 245
    }
1000
1001 245
    public function invalidateCache()
1002
    {
1003 245
        if ($this->cache !== null) {
1004 101
            $this->cache->delete($this->cacheKey);
1005 101
            $this->cache->delete($this->getUserRolesCacheKey());
1006 101
            $this->items = null;
1007 101
            $this->rules = null;
1008 101
            $this->parents = null;
1009
        }
1010 245
        $this->checkAccessAssignments = [];
1011 245
    }
1012
1013 30
    public function loadFromCache()
1014
    {
1015 30
        if ($this->items !== null || !$this->cache instanceof CacheInterface) {
1016 30
            return;
1017
        }
1018
1019 15
        $data = $this->cache->get($this->cacheKey);
1020 15
        if (is_array($data) && isset($data[0], $data[1], $data[2])) {
1021
            list($this->items, $this->rules, $this->parents) = $data;
1022
            return;
1023
        }
1024
1025 15
        $query = (new Query())->from($this->itemTable);
1026 15
        $this->items = [];
1027 15
        foreach ($query->all($this->db) as $row) {
1028 15
            $this->items[$row['name']] = $this->populateItem($row);
1029
        }
1030
1031 15
        $query = (new Query())->from($this->ruleTable);
1032 15
        $this->rules = [];
1033 15
        foreach ($query->all($this->db) as $row) {
1034 15
            $data = $row['data'];
1035 15
            if (is_resource($data)) {
1036 7
                $data = stream_get_contents($data);
1037
            }
1038 15
            if ($data) {
1039 15
                $this->rules[$row['name']] = unserialize($data);
1040
            }
1041
        }
1042
1043 15
        $query = (new Query())->from($this->itemChildTable);
1044 15
        $this->parents = [];
1045 15
        foreach ($query->all($this->db) as $row) {
1046 7
            if (isset($this->items[$row['child']])) {
1047 7
                $this->parents[$row['child']][] = $row['parent'];
1048
            }
1049
        }
1050
1051 15
        $this->cache->set($this->cacheKey, [$this->items, $this->rules, $this->parents]);
1052 15
    }
1053
1054
    /**
1055
     * Returns all role assignment information for the specified role.
1056
     * @param string $roleName
1057
     * @return string[] the ids. An empty array will be
1058
     * returned if role is not assigned to any user.
1059
     * @since 2.0.7
1060
     */
1061 5
    public function getUserIdsByRole($roleName)
1062
    {
1063 5
        if (empty($roleName)) {
1064
            return [];
1065
        }
1066
1067 5
        return (new Query())->select('[[user_id]]')
1068 5
            ->from($this->assignmentTable)
1069 5
            ->where(['item_name' => $roleName])->column($this->db);
1070
    }
1071
1072
    /**
1073
     * Check whether $userId is empty.
1074
     * @param mixed $userId
1075
     * @return bool
1076
     * @since 2.0.26
1077
     */
1078 140
    protected function isEmptyUserId($userId)
1079
    {
1080 140
        return !isset($userId) || $userId === '';
1081
    }
1082
1083 101
    private function getUserRolesCacheKey()
1084
    {
1085 101
        return $this->cacheKey . $this->rolesCacheSuffix;
1086
    }
1087
}
1088