Completed
Push — master ( 2aab0a...440703 )
by Raffael
19:06 queued 15:05
created

User   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 701
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 4
dl 0
loc 701
ccs 0
cts 226
cp 0
rs 4.4589
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A __toString() 0 4 1
A updateIdentity() 0 36 5
A setAttributes() 0 10 2
A getAttributes() 0 18 1
A getShares() 0 15 2
A getNodeAttributeSummary() 0 28 4
A getFilesystem() 0 8 2
A isAdmin() 0 4 1
A hasShare() 0 10 1
D updateShares() 0 130 15
A getId() 0 4 1
A getNamespace() 0 4 1
A getHardQuota() 0 4 1
A setHardQuota() 0 7 1
A setSoftQuota() 0 7 1
A save() 0 18 2
A getQuotaUsage() 0 32 2
A checkQuota() 0 14 3
A delete() 0 19 3
A undelete() 0 6 1
A isDeleted() 0 4 1
A getUsername() 0 4 1
A getGroups() 0 4 1
A getResolvedGroups() 0 6 1
A _getAttributeSummary() 0 44 2

How to fix   Complexity   

Complex Class

Complex classes like User 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 User, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Server;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Exception;
16
use Balloon\Filesystem\Node\Collection;
17
use Balloon\Server;
18
use Generator;
19
use Micro\Auth\Identity;
20
use MongoDB\BSON\Binary;
21
use MongoDB\BSON\ObjectId;
22
use MongoDB\BSON\UTCDateTime;
23
use MongoDB\Database;
24
use MongoDB\Driver\Exception\BulkWriteException;
25
use Psr\Log\LoggerInterface;
26
27
class User implements RoleInterface
28
{
29
    /**
30
     * User unique id.
31
     *
32
     * @var ObjectId
33
     */
34
    protected $_id;
35
36
    /**
37
     * Username.
38
     *
39
     * @var string
40
     */
41
    protected $username;
42
43
    /**
44
     * Optional user attributes.
45
     *
46
     * @var array
47
     */
48
    protected $optional = [];
49
50
    /**
51
     * Locale.
52
     *
53
     * @var string
54
     */
55
    protected $locale = 'en_US';
56
57
    /**
58
     * Groups.
59
     *
60
     * @var array
61
     */
62
    protected $groups = [];
63
64
    /**
65
     * Last sync timestamp.
66
     *
67
     * @var UTCDateTime
68
     */
69
    protected $last_attr_sync;
70
71
    /**
72
     * Soft Quota.
73
     *
74
     * @var int
75
     */
76
    protected $soft_quota = -1;
77
78
    /**
79
     * Hard Quota.
80
     *
81
     * @var int
82
     */
83
    protected $hard_quota = -1;
84
85
    /**
86
     * Is user deleted?
87
     *
88
     * @var bool|UTCDateTime
89
     */
90
    protected $deleted = false;
91
92
    /**
93
     * Admin.
94
     *
95
     * @var bool
96
     */
97
    protected $admin = false;
98
99
    /**
100
     * Created.
101
     *
102
     * @var UTCDateTime
103
     */
104
    protected $created;
105
106
    /**
107
     * Changed.
108
     *
109
     * @var UTCDateTime
110
     */
111
    protected $changed;
112
113
    /**
114
     * avatar.
115
     *
116
     * @var Binary
117
     */
118
    protected $avatar;
119
120
    /**
121
     * Namespace.
122
     *
123
     * @var string
124
     */
125
    protected $namespace;
126
127
    /**
128
     * Mail.
129
     *
130
     * @var string
131
     */
132
    protected $mail;
133
134
    /**
135
     * Db.
136
     *
137
     * @var Database
138
     */
139
    protected $db;
140
141
    /**
142
     * LoggerInterface.
143
     *
144
     * @var LoggerInterface
145
     */
146
    protected $logger;
147
148
    /**
149
     * Server.
150
     *
151
     * @var Server
152
     */
153
    protected $server;
154
155
    /**
156
     * Password.
157
     *
158
     * @var string
159
     */
160
    protected $password;
161
162
    /**
163
     * Filesystem.
164
     *
165
     * @var Filesystem
166
     */
167
    protected $fs;
168
169
    /**
170
     * Instance user.
171
     */
172
    public function __construct(array $attributes, Server $server, Database $db, LoggerInterface $logger)
173
    {
174
        $this->server = $server;
175
        $this->db = $db;
176
        $this->logger = $logger;
177
178
        foreach ($attributes as $attr => $value) {
179
            $this->{$attr} = $value;
180
        }
181
    }
182
183
    /**
184
     * Return username as string.
185
     */
186
    public function __toString(): string
187
    {
188
        return $this->username;
189
    }
190
191
    /**
192
     * Update user with identity attributes.
193
     *
194
     *
195
     * @return User
196
     */
197
    public function updateIdentity(Identity $identity): self
198
    {
199
        $attr_sync = $identity->getAdapter()->getAttributeSyncCache();
200
        if ($attr_sync === -1) {
201
            return $this;
202
        }
203
204
        $cache = ($this->last_attr_sync instanceof UTCDateTime ?
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
205
            $this->last_attr_sync->toDateTime()->format('U') : 0);
206
207
        if (time() - $attr_sync > $cache) {
208
            $this->logger->info('user attribute sync cache time expired, resync with auth attributes', [
209
                'category' => get_class($this),
210
            ]);
211
212
            $attributes = $identity->getAttributes();
213
            foreach ($attributes as $attr => $value) {
214
                $this->{$attr} = $value;
215
            }
216
217
            $this->last_attr_sync = new UTCDateTime();
218
219
            $save = array_keys($attributes);
220
            $save[] = 'last_attr_sync';
221
222
            $this->save($save);
223
224
            return $this;
225
        }
226
227
        $this->logger->debug('user auth attribute sync cache is in time', [
228
            'category' => get_class($this),
229
        ]);
230
231
        return $this;
232
    }
233
234
    /**
235
     * Set user attributes.
236
     */
237
    public function setAttributes(array $attributes = []): bool
238
    {
239
        $attributes = $this->server->validateUserAttributes($attributes);
240
241
        foreach ($attributes as $attr => $value) {
242
            $this->{$attr} = $value;
243
        }
244
245
        return $this->save(array_keys($attributes));
246
    }
247
248
    /**
249
     * Get Attributes.
250
     */
251
    public function getAttributes(): array
252
    {
253
        return [
254
            '_id' => $this->_id,
255
            'username' => $this->username,
256
            'locale' => $this->locale,
257
            'namespace' => $this->namespace,
258
            'created' => $this->created,
259
            'changed' => $this->changed,
260
            'deleted' => $this->deleted,
261
            'soft_quota' => $this->soft_quota,
262
            'hard_quota' => $this->hard_quota,
263
            'mail' => $this->mail,
264
            'admin' => $this->admin,
265
            'optional' => $this->optional,
266
            'avatar' => $this->avatar,
267
        ];
268
    }
269
270
    /**
271
     * Find all shares with membership.
272
     */
273
    public function getShares(): array
274
    {
275
        $result = $this->getFilesystem()->findNodesByFilter([
276
            'deleted' => false,
277
            'shared' => true,
278
            'owner' => $this->_id,
279
        ]);
280
281
        $list = [];
282
        foreach ($result as $node) {
283
            $list[] = $node->getShareId();
284
        }
285
286
        return $list;
287
    }
288
289
    /**
290
     * Get node attribute usage.
291
     */
292
    public function getNodeAttributeSummary($attributes = [], int $limit = 25): array
293
    {
294
        $mongodb = $this->db->storage;
0 ignored issues
show
Unused Code introduced by
$mongodb is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
295
296
        $valid = [
297
            'mime' => 'string',
298
            'meta.tags' => 'array',
299
            'meta.author' => 'string',
300
            'meta.color' => 'string',
301
            'meta.license' => 'string',
302
            'meta.copyright' => 'string',
303
        ];
304
305
        if (empty($attributes)) {
306
            $attributes = array_keys($valid);
307
        } elseif (is_string($attributes)) {
308
            $attributes = [$attributes];
309
        }
310
311
        $filter = array_intersect_key($valid, array_flip($attributes));
312
        $result = [];
313
314
        foreach ($filter as $attribute => $type) {
315
            $result[$attribute] = $this->_getAttributeSummary($attribute, $type, $limit);
316
        }
317
318
        return $result;
319
    }
320
321
    /**
322
     * Get filesystem.
323
     */
324
    public function getFilesystem(): Filesystem
325
    {
326
        if ($this->fs instanceof Filesystem) {
327
            return $this->fs;
328
        }
329
330
        return $this->fs = $this->server->getFilesystem($this);
331
    }
332
333
    /**
334
     * Is Admin user?
335
     */
336
    public function isAdmin(): bool
337
    {
338
        return $this->admin;
339
    }
340
341
    /**
342
     * Check if user has share.
343
     */
344
    public function hasShare(Collection $node): bool
345
    {
346
        $result = $this->db->storage->count([
347
            'reference' => $node->getId(),
348
            'directory' => true,
349
            'owner' => $this->_id,
350
        ]);
351
352
        return 1 === $result;
353
    }
354
355
    /**
356
     * Find new shares and create reference.
357
     */
358
    public function updateShares(): self
359
    {
360
        $item = $this->db->storage->find([
361
            'deleted' => false,
362
            'shared' => true,
363
            'directory' => true,
364
            '$or' => [
365
                ['acl' => [
366
                    '$elemMatch' => [
367
                        'id' => (string) $this->_id,
368
                        'type' => 'user',
369
                    ],
370
                ]],
371
                ['acl' => [
372
                    '$elemMatch' => [
373
                        'id' => ['$in' => array_map('strval', $this->groups)],
374
                        'type' => 'group',
375
                    ],
376
                ]],
377
            ],
378
        ]);
379
380
        $found = [];
381
        $list = [];
382
        foreach ($item as $child) {
383
            $found[] = $child['_id'];
384
            $list[(string) $child['_id']] = $child;
385
        }
386
387
        if (empty($found)) {
388
            return $this;
389
        }
390
391
        //check for references
392
        $item = $this->db->storage->find([
393
            'directory' => true,
394
            'shared' => true,
395
            'owner' => $this->_id,
396
            'reference' => ['$exists' => 1],
397
        ]);
398
399
        $exists = [];
400
        foreach ($item as $child) {
401
            if (!in_array($child['reference'], $found)) {
402
                $this->logger->debug('found dead reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
403
                    'category' => get_class($this),
404
                ]);
405
406
                try {
407
                    $this->getFilesystem()->findNodeById($child['_id'])->delete(true);
408
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
409
                }
410
            } else {
411
                $this->logger->debug('found existing share reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
412
                    'category' => get_class($this),
413
                ]);
414
415
                $exists[] = $child['reference'];
416
            }
417
        }
418
419
        $new = array_diff($found, $exists);
420
        foreach ($new as $add) {
0 ignored issues
show
Bug introduced by
The expression $new of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
421
            $node = $list[(string) $add];
422
            $lookup = array_column($node['acl'], null, 'privilege');
423
            if (isset($lookup['d'])) {
424
                $this->logger->debug('ignore share ['.$node['_id'].'] with deny privilege', [
425
                    'category' => get_class($this),
426
                ]);
427
428
                continue;
429
            }
430
431
            $this->logger->info('found new share ['.$node['_id'].']', [
432
                'category' => get_class($this),
433
            ]);
434
435
            if ($node['owner'] == $this->_id) {
436
                $this->logger->debug('skip creating reference to share ['.$node['_id'].'] cause share owner ['.$node['owner'].'] is the current user', [
437
                    'category' => get_class($this),
438
                ]);
439
440
                continue;
441
            }
442
443
            $attrs = [
444
                'shared' => true,
445
                'parent' => null,
446
                'reference' => $node['_id'],
447
                'pointer' => $node['_id'],
448
            ];
449
450
            $dir = $this->getFilesystem()->getRoot();
451
452
            try {
453
                $dir->addDirectory($node['share_name'], $attrs);
454
            } catch (Exception\Conflict $e) {
455
                $conflict_node = $dir->getChild($node['share_name']);
456
457
                if (!$conflict_node->isReference() && $conflict_node->getShareId() != $attrs['reference']) {
458
                    $new = $node['share_name'].' ('.substr(uniqid('', true), -4).')';
459
                    $dir->addDirectory($new, $attrs);
460
                }
461
            } catch (BulkWriteException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\BulkWriteException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
462
                if ($e->getCode() !== 11000) {
463
                    throw $e;
464
                }
465
466
                $this->logger->warning('share reference to ['.$node['_id'].'] has already been created', [
467
                    'category' => get_class($this),
468
                    'exception' => $e,
469
                ]);
470
471
                continue;
472
            } catch (\Exception $e) {
473
                $this->logger->error('failed create new share reference to share ['.$node['_id'].']', [
474
                    'category' => get_class($this),
475
                    'exception' => $e,
476
                ]);
477
478
                throw $e;
479
            }
480
481
            $this->logger->info('created new share reference to share ['.$node['_id'].']', [
482
                'category' => get_class($this),
483
            ]);
484
        }
485
486
        return $this;
487
    }
488
489
    /**
490
     * Get unique id.
491
     */
492
    public function getId(): ObjectId
493
    {
494
        return $this->_id;
495
    }
496
497
    /**
498
     * Get namespace.
499
     */
500
    public function getNamespace(): ?string
501
    {
502
        return $this->namespace;
503
    }
504
505
    /**
506
     * Get hard quota.
507
     */
508
    public function getHardQuota(): int
509
    {
510
        return $this->hard_quota;
511
    }
512
513
    /**
514
     * Set hard quota.
515
     */
516
    public function setHardQuota(int $quota): self
517
    {
518
        $this->hard_quota = (int) $quota;
519
        $this->save(['hard_quota']);
520
521
        return $this;
522
    }
523
524
    /**
525
     * Set soft quota.
526
     */
527
    public function setSoftQuota(int $quota): self
528
    {
529
        $this->soft_quota = (int) $quota;
530
        $this->save(['soft_quota']);
531
532
        return $this;
533
    }
534
535
    /**
536
     * Save.
537
     */
538
    public function save(array $attributes = []): bool
539
    {
540
        $this->changed = new UTCDateTime();
541
        $attributes[] = 'changed';
542
543
        $set = [];
544
        foreach ($attributes as $attr) {
545
            $set[$attr] = $this->{$attr};
546
        }
547
548
        $result = $this->db->user->updateOne([
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
549
            '_id' => $this->_id,
550
        ], [
551
            '$set' => $set,
552
        ]);
553
554
        return true;
555
    }
556
557
    /**
558
     * Get used qota.
559
     */
560
    public function getQuotaUsage(): array
561
    {
562
        $result = $this->db->storage->aggregate([
563
            [
564
                '$match' => [
565
                    'owner' => $this->_id,
566
                    'directory' => false,
567
                    'deleted' => false,
568
                    'storage_reference' => null,
569
                ],
570
            ],
571
            [
572
                '$group' => [
573
                    '_id' => null,
574
                    'sum' => ['$sum' => '$size'],
575
                ],
576
            ],
577
        ]);
578
579
        $result = iterator_to_array($result);
580
        $sum = 0;
581
        if (isset($result[0]['sum'])) {
582
            $sum = $result[0]['sum'];
583
        }
584
585
        return [
586
            'used' => $sum,
587
            'available' => ($this->hard_quota - $sum),
588
            'hard_quota' => $this->hard_quota,
589
            'soft_quota' => $this->soft_quota,
590
        ];
591
    }
592
593
    /**
594
     * Check quota.
595
     */
596
    public function checkQuota(int $add): bool
597
    {
598
        if ($this->hard_quota === -1) {
599
            return true;
600
        }
601
602
        $quota = $this->getQuotaUsage();
603
604
        if (($quota['used'] + $add) > $quota['hard_quota']) {
605
            return false;
606
        }
607
608
        return true;
609
    }
610
611
    /**
612
     * Delete user.
613
     */
614
    public function delete(bool $force = false, bool $data = false, bool $force_data = false): bool
615
    {
616
        if (false === $force) {
617
            $this->deleted = new UTCDateTime();
618
            $result = $this->save(['deleted']);
619
        } else {
620
            $result = $this->db->user->deleteOne([
621
                '_id' => $this->_id,
622
            ]);
623
624
            $result = $result->isAcknowledged();
625
        }
626
627
        if ($data === true) {
628
            $this->getFilesystem()->getRoot()->delete($force_data);
629
        }
630
631
        return $result;
632
    }
633
634
    /**
635
     * Undelete user.
636
     */
637
    public function undelete(): bool
638
    {
639
        $this->deleted = false;
640
641
        return $this->save(['deleted']);
642
    }
643
644
    /**
645
     * Check if user is deleted.
646
     */
647
    public function isDeleted(): bool
648
    {
649
        return $this->deleted instanceof UTCDateTime;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
650
    }
651
652
    /**
653
     * Get Username.
654
     */
655
    public function getUsername(): string
656
    {
657
        return $this->username;
658
    }
659
660
    /**
661
     * Get groups.
662
     */
663
    public function getGroups(): array
664
    {
665
        return $this->groups;
666
    }
667
668
    /**
669
     * Get resolved groups.
670
     *
671
     * @return Generator
672
     */
673
    public function getResolvedGroups(?int $offset = null, ?int $limit = null): ?Generator
674
    {
675
        return $this->server->getGroups([
676
            '_id' => ['$in' => $this->groups],
677
        ], $offset, $limit);
678
    }
679
680
    /**
681
     * Get attribute usage summary.
682
     */
683
    protected function _getAttributeSummary(string $attribute, string $type = 'string', int $limit = 25): array
684
    {
685
        $mongodb = $this->db->storage;
686
        $ops = [
687
            [
688
                '$match' => [
689
                    '$and' => [
690
                        ['deleted' => false],
691
                        [$attribute => ['$exists' => true]],
692
                        ['$or' => [
693
                            ['owner' => $this->getId()],
694
                            ['shared' => ['$in' => $this->getShares()]],
695
                        ]],
696
                    ],
697
                ],
698
            ],
699
        ];
700
701
        if ('array' === $type) {
702
            $ops[] = [
703
                '$unwind' => '$'.$attribute,
704
            ];
705
        }
706
707
        $ops[] = [
708
            '$group' => [
709
                '_id' => '$'.$attribute,
710
                'sum' => ['$sum' => 1],
711
            ],
712
        ];
713
714
        $ops[] = [
715
            '$sort' => [
716
               'sum' => -1,
717
               '_id' => 1,
718
            ],
719
        ];
720
721
        $ops[] = [
722
            '$limit' => $limit,
723
        ];
724
725
        return $mongodb->aggregate($ops)->toArray();
726
    }
727
}
728