Passed
Pull Request — master (#19513)
by Alexander
17:27 queued 09:05
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
     */
93
    public $rolesCacheSuffix = 'roles';
94
95
    /**
96
     * @var Item[] all auth items (name => Item)
97
     */
98
    protected $items;
99
    /**
100
     * @var Rule[] all auth rules (name => Rule)
101
     */
102
    protected $rules;
103
    /**
104
     * @var array auth item parent-child relationships (childName => list of parents)
105
     */
106
    protected $parents;
107
    /**
108
     * @var array user assignments (user id => Assignment[])
109
     * @since `protected` since 2.0.38
110
     */
111
    protected $checkAccessAssignments = [];
112
113
114
    /**
115
     * Initializes the application component.
116
     * This method overrides the parent implementation by establishing the database connection.
117
     */
118 245
    public function init()
119
    {
120 245
        parent::init();
121 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

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

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