Test Failed
Push — master ( 8c814b...380005 )
by Raffael
08:49
created

User::updateShares()   D

Complexity

Conditions 15
Paths 74

Size

Total Lines 130

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 0
Metric Value
dl 0
loc 130
ccs 0
cts 66
cp 0
rs 4.7333
c 0
b 0
f 0
cc 15
nc 74
nop 0
crap 240

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 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\Hook;
18
use Balloon\Server;
19
use Generator;
20
use Micro\Auth\Identity;
21
use MongoDB\BSON\Binary;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use MongoDB\Database;
25
use MongoDB\Driver\Exception\BulkWriteException;
26
use Psr\Log\LoggerInterface;
27
28
class User implements RoleInterface
29
{
30
    /**
31
     * User unique id.
32
     *
33
     * @var ObjectId
34
     */
35
    protected $_id;
36
37
    /**
38
     * Username.
39
     *
40
     * @var string
41
     */
42
    protected $username;
43
44
    /**
45
     * Locale.
46
     *
47
     * @var string
48
     */
49
    protected $locale = 'en_US';
50
51
    /**
52
     * Groups.
53
     *
54
     * @var array
55
     */
56
    protected $groups = [];
57
58
    /**
59
     * Last sync timestamp.
60
     *
61
     * @var UTCDateTime
62
     */
63
    protected $last_attr_sync;
64
65
    /**
66
     * Soft Quota.
67
     *
68
     * @var int
69
     */
70
    protected $soft_quota = -1;
71
72
    /**
73
     * Hard Quota.
74
     *
75
     * @var int
76
     */
77
    protected $hard_quota = -1;
78
79
    /**
80
     * Is user deleted?
81
     *
82
     * @var bool|UTCDateTime
83
     */
84
    protected $deleted = false;
85
86
    /**
87
     * Admin.
88
     *
89
     * @var bool
90
     */
91
    protected $admin = false;
92
93
    /**
94
     * Multi factor auth.
95
     *
96
     * @var bool
97
     */
98
    protected $multi_factor_auth = false;
99
100
    /**
101
     * Created.
102
     *
103
     * @var UTCDateTime
104
     */
105
    protected $created;
106
107
    /**
108
     * Changed.
109
     *
110
     * @var UTCDateTime
111
     */
112
    protected $changed;
113
114
    /**
115
     * avatar.
116
     *
117
     * @var Binary
118
     */
119
    protected $avatar;
120
121
    /**
122
     * Namespace.
123
     *
124
     * @var string
125
     */
126
    protected $namespace;
127
128
    /**
129
     * Mail.
130
     *
131
     * @var string
132
     */
133
    protected $mail;
134
135
    /**
136
     * Db.
137
     *
138
     * @var Database
139
     */
140
    protected $db;
141
142
    /**
143
     * LoggerInterface.
144
     *
145
     * @var LoggerInterface
146
     */
147
    protected $logger;
148
149
    /**
150
     * Server.
151
     *
152
     * @var Server
153
     */
154
    protected $server;
155
156
    /**
157
     * Password.
158
     *
159
     * @var string
160
     */
161
    protected $password;
162
163
    /**
164
     * Filesystem.
165
     *
166
     * @var Filesystem
167
     */
168
    protected $fs;
169
170
    /**
171
     * Identity.
172
     *
173
     * @var Identity
174
     */
175
    protected $identity;
176
177
    /**
178
     * Hook.
179
     *
180
     * @var Hook
181
     */
182
    protected $hook;
183
184
    /**
185
     * Google auth secret.
186
     *
187
     * @var string
188
     */
189
    protected $google_auth_secret;
190
191
    /**
192
     * Instance user.
193
     */
194
    public function __construct(array $attributes, Server $server, Database $db, Hook $hook, LoggerInterface $logger)
195
    {
196
        $this->server = $server;
197
        $this->db = $db;
198
        $this->logger = $logger;
199
        $this->hook = $hook;
200
201
        foreach ($attributes as $attr => $value) {
202
            $this->{$attr} = $value;
203
        }
204
    }
205
206
    /**
207
     * Return username as string.
208
     */
209
    public function __toString(): string
210
    {
211
        return $this->username;
212
    }
213
214
    /**
215
     * Get identity.
216
     */
217
    public function getIdentity(): ?Identity
218
    {
219
        return $this->identity;
220
    }
221
222
    /**
223
     * Update user with identity attributes.
224
     */
225
    public function updateIdentity(Identity $identity): self
226
    {
227
        $this->identity = $identity;
228
        $attr_sync = $identity->getAdapter()->getAttributeSyncCache();
229
        if ($attr_sync === -1) {
230
            return $this;
231
        }
232
233
        $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...
234
            $this->last_attr_sync->toDateTime()->format('U') : 0);
235
236
        if (time() - $attr_sync > $cache) {
237
            $this->logger->info('user attribute sync cache time expired, resync with auth attributes', [
238
                'category' => get_class($this),
239
            ]);
240
241
            $attributes = $identity->getAttributes();
242
            foreach ($attributes as $attr => $value) {
243
                $this->{$attr} = $value;
244
            }
245
246
            $this->last_attr_sync = new UTCDateTime();
247
248
            $save = array_keys($attributes);
249
            $save[] = 'last_attr_sync';
250
251
            $this->save($save);
252
253
            return $this;
254
        }
255
256
        $this->logger->debug('user auth attribute sync cache is in time', [
257
            'category' => get_class($this),
258
        ]);
259
260
        return $this;
261
    }
262
263
    /**
264
     * Set user attributes.
265
     */
266
    public function setAttributes(array $attributes = []): bool
267
    {
268
        $this->hook->run('preUpdateUser', [$this, &$attributes]);
269
        $attributes = $this->server->validateUserAttributes($attributes);
270
271
        foreach ($attributes as $attr => $value) {
272
            $this->{$attr} = $value;
273
        }
274
275
        $result = $this->save(array_keys($attributes));
276
        $this->hook->run('postUpdateUser', [$this, $attributes]);
277
278
        return $result;
279
    }
280
281
    /**
282
     * Get Attributes.
283
     */
284
    public function getAttributes(): array
285
    {
286
        return [
287
            '_id' => $this->_id,
288
            'username' => $this->username,
289
            'locale' => $this->locale,
290
            'namespace' => $this->namespace,
291
            'created' => $this->created,
292
            'changed' => $this->changed,
293
            'deleted' => $this->deleted,
294
            'soft_quota' => $this->soft_quota,
295
            'hard_quota' => $this->hard_quota,
296
            'mail' => $this->mail,
297
            'admin' => $this->admin,
298
            'avatar' => $this->avatar,
299
            'multi_factor_auth' => $this->multi_factor_auth,
300
            'google_auth_secret' => $this->google_auth_secret,
301
        ];
302
    }
303
304
    /**
305
     * Find all shares with membership.
306
     */
307
    public function getShares(): array
308
    {
309
        $result = $this->getFilesystem()->findNodesByFilter([
310
            'deleted' => false,
311
            'shared' => true,
312
            'owner' => $this->_id,
313
        ]);
314
315
        $list = [];
316
        foreach ($result as $node) {
317
            $list[] = $node->getShareId();
318
        }
319
320
        return $list;
321
    }
322
323
    /**
324
     * Get node attribute usage.
325
     */
326
    public function getNodeAttributeSummary($attributes = [], int $limit = 25): array
327
    {
328
        $mongodb = $this->db->storage;
0 ignored issues
show
Bug introduced by
The property storage does not seem to exist in MongoDB\Database.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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...
329
330
        $valid = [
331
            'mime' => 'string',
332
            'meta.tags' => 'array',
333
            'meta.author' => 'string',
334
            'meta.color' => 'string',
335
            'meta.license' => 'string',
336
            'meta.copyright' => 'string',
337
        ];
338
339
        if (empty($attributes)) {
340
            $attributes = array_keys($valid);
341
        } elseif (is_string($attributes)) {
342
            $attributes = [$attributes];
343
        }
344
345
        $filter = array_intersect_key($valid, array_flip($attributes));
346
        $result = [];
347
348
        foreach ($filter as $attribute => $type) {
349
            $result[$attribute] = $this->_getAttributeSummary($attribute, $type, $limit);
350
        }
351
352
        return $result;
353
    }
354
355
    /**
356
     * Get filesystem.
357
     */
358
    public function getFilesystem(): Filesystem
359
    {
360
        if ($this->fs instanceof Filesystem) {
361
            return $this->fs;
362
        }
363
364
        return $this->fs = $this->server->getFilesystem($this);
365
    }
366
367
    /**
368
     * Is Admin user?
369
     */
370
    public function isAdmin(): bool
371
    {
372
        return $this->admin;
373
    }
374
375
    /**
376
     * Check if user has share.
377
     */
378
    public function hasShare(Collection $node): bool
379
    {
380
        $result = $this->db->storage->count([
381
            'reference' => $node->getId(),
382
            'directory' => true,
383
            'owner' => $this->_id,
384
        ]);
385
386
        return 1 === $result;
387
    }
388
389
    /**
390
     * Find new shares and create reference.
391
     */
392
    public function updateShares(): self
393
    {
394
        $item = $this->db->storage->find([
395
            'deleted' => false,
396
            'shared' => true,
397
            'directory' => true,
398
            '$or' => [
399
                ['acl' => [
400
                    '$elemMatch' => [
401
                        'id' => (string) $this->_id,
402
                        'type' => 'user',
403
                    ],
404
                ]],
405
                ['acl' => [
406
                    '$elemMatch' => [
407
                        'id' => ['$in' => array_map('strval', $this->groups)],
408
                        'type' => 'group',
409
                    ],
410
                ]],
411
            ],
412
        ]);
413
414
        $found = [];
415
        $list = [];
416
        foreach ($item as $child) {
417
            $found[] = $child['_id'];
418
            $list[(string) $child['_id']] = $child;
419
        }
420
421
        if (empty($found)) {
422
            return $this;
423
        }
424
425
        //check for references
426
        $item = $this->db->storage->find([
427
            'directory' => true,
428
            'shared' => true,
429
            'owner' => $this->_id,
430
            'reference' => ['$exists' => 1],
431
        ]);
432
433
        $exists = [];
434
        foreach ($item as $child) {
435
            if (!in_array($child['reference'], $found)) {
436
                $this->logger->debug('found dead reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
437
                    'category' => get_class($this),
438
                ]);
439
440
                try {
441
                    $this->getFilesystem()->findNodeById($child['_id'])->delete(true);
442
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
443
                }
444
            } else {
445
                $this->logger->debug('found existing share reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
446
                    'category' => get_class($this),
447
                ]);
448
449
                $exists[] = $child['reference'];
450
            }
451
        }
452
453
        $new = array_diff($found, $exists);
454
        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...
455
            $node = $list[(string) $add];
456
            $lookup = array_column($node['acl'], null, 'privilege');
457
            if (isset($lookup['d'])) {
458
                $this->logger->debug('ignore share ['.$node['_id'].'] with deny privilege', [
459
                    'category' => get_class($this),
460
                ]);
461
462
                continue;
463
            }
464
465
            $this->logger->info('found new share ['.$node['_id'].']', [
466
                'category' => get_class($this),
467
            ]);
468
469
            if ($node['owner'] == $this->_id) {
470
                $this->logger->debug('skip creating reference to share ['.$node['_id'].'] cause share owner ['.$node['owner'].'] is the current user', [
471
                    'category' => get_class($this),
472
                ]);
473
474
                continue;
475
            }
476
477
            $attrs = [
478
                'shared' => true,
479
                'parent' => null,
480
                'reference' => $node['_id'],
481
                'pointer' => $node['_id'],
482
            ];
483
484
            $dir = $this->getFilesystem()->getRoot();
485
486
            try {
487
                $dir->addDirectory($node['share_name'], $attrs);
488
            } catch (Exception\Conflict $e) {
489
                $conflict_node = $dir->getChild($node['share_name']);
490
491
                if (!$conflict_node->isReference() && $conflict_node->getShareId() != $attrs['reference']) {
492
                    $new = $node['share_name'].' ('.substr(uniqid('', true), -4).')';
493
                    $dir->addDirectory($new, $attrs);
494
                }
495
            } 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...
496
                if ($e->getCode() !== 11000) {
497
                    throw $e;
498
                }
499
500
                $this->logger->warning('share reference to ['.$node['_id'].'] has already been created', [
501
                    'category' => get_class($this),
502
                    'exception' => $e,
503
                ]);
504
505
                continue;
506
            } catch (\Exception $e) {
507
                $this->logger->error('failed create new share reference to share ['.$node['_id'].']', [
508
                    'category' => get_class($this),
509
                    'exception' => $e,
510
                ]);
511
512
                throw $e;
513
            }
514
515
            $this->logger->info('created new share reference to share ['.$node['_id'].']', [
516
                'category' => get_class($this),
517
            ]);
518
        }
519
520
        return $this;
521
    }
522
523
    /**
524
     * Get unique id.
525
     */
526
    public function getId(): ObjectId
527
    {
528
        return $this->_id;
529
    }
530
531
    /**
532
     * Get namespace.
533
     */
534
    public function getNamespace(): ?string
535
    {
536
        return $this->namespace;
537
    }
538
539
    /**
540
     * Get hard quota.
541
     */
542
    public function getHardQuota(): int
543
    {
544
        return $this->hard_quota;
545
    }
546
547
    /**
548
     * Set hard quota.
549
     */
550
    public function setHardQuota(int $quota): self
551
    {
552
        $this->hard_quota = (int) $quota;
553
        $this->save(['hard_quota']);
554
555
        return $this;
556
    }
557
558
    /**
559
     * Set soft quota.
560
     */
561
    public function setSoftQuota(int $quota): self
562
    {
563
        $this->soft_quota = (int) $quota;
564
        $this->save(['soft_quota']);
565
566
        return $this;
567
    }
568
569
    /**
570
     * Save.
571
     */
572
    public function save(array $attributes = []): bool
573
    {
574
        $this->changed = new UTCDateTime();
575
        $attributes[] = 'changed';
576
577
        $set = [];
578
        foreach ($attributes as $attr) {
579
            $set[$attr] = $this->{$attr};
580
        }
581
582
        $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...
583
            '_id' => $this->_id,
584
        ], [
585
            '$set' => $set,
586
        ]);
587
588
        return true;
589
    }
590
591
    /**
592
     * Get used qota.
593
     */
594
    public function getQuotaUsage(): array
595
    {
596
        $result = $this->db->storage->aggregate([
597
            [
598
                '$match' => [
599
                    'owner' => $this->_id,
600
                    'directory' => false,
601
                    'deleted' => false,
602
                    'storage_reference' => null,
603
                ],
604
            ],
605
            [
606
                '$group' => [
607
                    '_id' => null,
608
                    'sum' => ['$sum' => '$size'],
609
                ],
610
            ],
611
        ]);
612
613
        $result = iterator_to_array($result);
614
        $sum = 0;
615
        if (isset($result[0]['sum'])) {
616
            $sum = $result[0]['sum'];
617
        }
618
619
        return [
620
            'used' => $sum,
621
            'available' => ($this->hard_quota - $sum),
622
            'hard_quota' => $this->hard_quota,
623
            'soft_quota' => $this->soft_quota,
624
        ];
625
    }
626
627
    /**
628
     * Check quota.
629
     */
630
    public function checkQuota(int $add): bool
631
    {
632
        if ($this->hard_quota === -1) {
633
            return true;
634
        }
635
636
        $quota = $this->getQuotaUsage();
637
638
        if (($quota['used'] + $add) > $quota['hard_quota']) {
639
            return false;
640
        }
641
642
        return true;
643
    }
644
645
    /**
646
     * Delete user.
647
     */
648
    public function delete(bool $force = false, bool $data = false, bool $force_data = false): bool
649
    {
650
        if (false === $force) {
651
            $this->deleted = new UTCDateTime();
652
            $result = $this->save(['deleted']);
653
        } else {
654
            $result = $this->db->user->deleteOne([
655
                '_id' => $this->_id,
656
            ]);
657
658
            $result = $result->isAcknowledged();
659
        }
660
661
        if ($data === true) {
662
            $this->getFilesystem()->getRoot()->delete($force_data);
663
        }
664
665
        return $result;
666
    }
667
668
    /**
669
     * Has password.
670
     */
671
    public function hasPassword(): bool
672
    {
673
        return $this->password !== null;
674
    }
675
676
    /**
677
     * Undelete user.
678
     */
679
    public function undelete(): bool
680
    {
681
        $this->deleted = false;
682
683
        return $this->save(['deleted']);
684
    }
685
686
    /**
687
     * Check if user is deleted.
688
     */
689
    public function isDeleted(): bool
690
    {
691
        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...
692
    }
693
694
    /**
695
     * Get Username.
696
     */
697
    public function getUsername(): string
698
    {
699
        return $this->username;
700
    }
701
702
    /**
703
     * Get groups.
704
     */
705
    public function getGroups(): array
706
    {
707
        return $this->groups;
708
    }
709
710
    /**
711
     * Get resolved groups.
712
     *
713
     * @return Generator
714
     */
715
    public function getResolvedGroups(?int $offset = null, ?int $limit = null): ?Generator
716
    {
717
        return $this->server->getGroups([
718
            '_id' => ['$in' => $this->groups],
719
        ], $offset, $limit);
720
    }
721
722
    /**
723
     * Get attribute usage summary.
724
     */
725
    protected function _getAttributeSummary(string $attribute, string $type = 'string', int $limit = 25): array
726
    {
727
        $mongodb = $this->db->storage;
0 ignored issues
show
Bug introduced by
The property storage does not seem to exist in MongoDB\Database.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
728
        $ops = [
729
            [
730
                '$match' => [
731
                    '$and' => [
732
                        ['deleted' => false],
733
                        [$attribute => ['$exists' => true]],
734
                        ['$or' => [
735
                            ['owner' => $this->getId()],
736
                            ['shared' => ['$in' => $this->getShares()]],
737
                        ]],
738
                    ],
739
                ],
740
            ],
741
        ];
742
743
        if ('array' === $type) {
744
            $ops[] = [
745
                '$unwind' => '$'.$attribute,
746
            ];
747
        }
748
749
        $ops[] = [
750
            '$group' => [
751
                '_id' => '$'.$attribute,
752
                'sum' => ['$sum' => 1],
753
            ],
754
        ];
755
756
        $ops[] = [
757
            '$sort' => [
758
               'sum' => -1,
759
               '_id' => 1,
760
            ],
761
        ];
762
763
        $ops[] = [
764
            '$limit' => $limit,
765
        ];
766
767
        return array_values($mongodb->aggregate($ops)->toArray());
768
    }
769
}
770