Passed
Push — scrutinizer-migrate-to-new-eng... ( 58afd6 )
by Alexander
18:11
created

DbManager   F

Complexity

Total Complexity 136

Size/Duplication

Total Lines 1014
Duplicated Lines 0 %

Test Coverage

Coverage 93.78%

Importance

Changes 0
Metric Value
eloc 422
dl 0
loc 1014
ccs 437
cts 466
cp 0.9378
rs 2
c 0
b 0
f 0
wmc 136

46 Methods

Rating   Name   Duplication   Size   Complexity  
A removeRule() 0 15 2
A getAssignments() 0 20 3
A getItems() 0 12 2
A updateRule() 0 22 3
A removeAll() 0 7 1
A getAssignment() 0 18 3
A getDirectPermissionsByUser() 0 14 2
A assign() 0 17 1
A hasChild() 0 6 1
A removeAllAssignments() 0 4 1
A isEmptyUserId() 0 3 2
A updateItem() 0 30 4
A removeAllItems() 0 24 4
B checkAccessRecursive() 0 28 8
A removeAllRules() 0 11 2
A getChildrenRecursive() 0 6 3
A addItem() 0 23 4
A getPermissionsByUser() 0 10 2
B checkAccessFromCache() 0 27 9
A getInheritedPermissionsByUser() 0 26 4
A getUserIdsByRole() 0 9 2
A addChild() 0 21 5
A revokeAll() 0 10 2
A invalidateCache() 0 9 2
A checkAccess() 0 19 4
A removeChildren() 0 9 1
A removeAllPermissions() 0 3 1
A detectLoop() 0 12 4
A getRule() 0 19 5
A getPermissionsByRole() 0 18 3
A removeItem() 0 18 2
B loadFromCache() 0 37 10
A getChildrenList() 0 9 2
A getChildren() 0 13 2
A revoke() 0 10 2
A getRolesByUser() 0 18 3
A getChildRoles() 0 18 2
A populateItem() 0 16 6
A removeChild() 0 9 1
A init() 0 6 2
A removeAllRoles() 0 3 1
A getRules() 0 18 4
A canAddChild() 0 3 1
A getItem() 0 19 4
A supportsCascadeUpdate() 0 3 1
A addRule() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like DbManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DbManager, and based on these observations, apply Extract Interface, too.

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\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 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
    /**
92
     * @var Item[] all auth items (name => Item)
93
     */
94
    protected $items;
95
    /**
96
     * @var Rule[] all auth rules (name => Rule)
97
     */
98
    protected $rules;
99
    /**
100
     * @var array auth item parent-child relationships (childName => list of parents)
101
     */
102
    protected $parents;
103
104
105
    /**
106
     * Initializes the application component.
107
     * This method overrides the parent implementation by establishing the database connection.
108
     */
109 245
    public function init()
110
    {
111 245
        parent::init();
112 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

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

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