Completed
Push — master ( 3e09cb...37faaa )
by Raffael
06:48 queued 02:27
created

User::updateShares()   F

Complexity

Conditions 18
Paths 130

Size

Total Lines 132

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 342

Importance

Changes 0
Metric Value
dl 0
loc 132
ccs 0
cts 66
cp 0
rs 3.6933
c 0
b 0
f 0
cc 18
nc 130
nop 0
crap 342

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