Passed
Push — master ( 2c25c6...9f902b )
by Raffael
08:57
created

User::getNodeAttributeSummary()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 0
cts 12
cp 0
rs 8.5806
c 0
b 0
f 0
cc 4
crap 20
eloc 18
nc 6
nop 2
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 Psr\Log\LoggerInterface;
25
26
class User implements RoleInterface
27
{
28
    /**
29
     * User unique id.
30
     *
31
     * @var ObjectId
32
     */
33
    protected $_id;
34
35
    /**
36
     * Username.
37
     *
38
     * @var string
39
     */
40
    protected $username;
41
42
    /**
43
     * Optional user attributes.
44
     *
45
     * @var array
46
     */
47
    protected $optional = [];
48
49
    /**
50
     * Locale.
51
     *
52
     * @var string
53
     */
54
    protected $locale = 'en_US';
55
56
    /**
57
     * Groups.
58
     *
59
     * @var array
60
     */
61
    protected $groups = [];
62
63
    /**
64
     * Last sync timestamp.
65
     *
66
     * @var UTCDateTime
67
     */
68
    protected $last_attr_sync;
69
70
    /**
71
     * Soft Quota.
72
     *
73
     * @var int
74
     */
75
    protected $soft_quota = -1;
76
77
    /**
78
     * Hard Quota.
79
     *
80
     * @var int
81
     */
82
    protected $hard_quota = -1;
83
84
    /**
85
     * Is user deleted?
86
     *
87
     * @var bool|UTCDateTime
88
     */
89
    protected $deleted = false;
90
91
    /**
92
     * Admin.
93
     *
94
     * @var bool
95
     */
96
    protected $admin = false;
97
98
    /**
99
     * Created.
100
     *
101
     * @var UTCDateTime
102
     */
103
    protected $created;
104
105
    /**
106
     * Changed.
107
     *
108
     * @var UTCDateTime
109
     */
110
    protected $changed;
111
112
    /**
113
     * avatar.
114
     *
115
     * @var Binary
116
     */
117
    protected $avatar;
118
119
    /**
120
     * Namespace.
121
     *
122
     * @var string
123
     */
124
    protected $namespace;
125
126
    /**
127
     * Mail.
128
     *
129
     * @var string
130
     */
131
    protected $mail;
132
133
    /**
134
     * Db.
135
     *
136
     * @var Database
137
     */
138
    protected $db;
139
140
    /**
141
     * LoggerInterface.
142
     *
143
     * @var LoggerInterface
144
     */
145
    protected $logger;
146
147
    /**
148
     * Server.
149
     *
150
     * @var Server
151
     */
152
    protected $server;
153
154
    /**
155
     * Password.
156
     *
157
     * @var string
158
     */
159
    protected $password;
160
161
    /**
162
     * Filesystem.
163
     *
164
     * @var Filesystem
165
     */
166
    protected $fs;
167
168
    /**
169
     * Instance user.
170
     *
171
     * @param array           $attributes
172
     * @param Server          $server
173
     * @param Database        $db
174
     * @param LoggerInterface $logger
175
     */
176
    public function __construct(array $attributes, Server $server, Database $db, LoggerInterface $logger)
177
    {
178
        $this->server = $server;
179
        $this->db = $db;
180
        $this->logger = $logger;
181
182
        foreach ($attributes as $attr => $value) {
183
            $this->{$attr} = $value;
184
        }
185
    }
186
187
    /**
188
     * Return username as string.
189
     *
190
     * @return string
191
     */
192
    public function __toString(): string
193
    {
194
        return $this->username;
195
    }
196
197
    /**
198
     * Update user with identity attributes.
199
     *
200
     * @param Identity $identity
201
     *
202
     * @return User
203
     */
204
    public function updateIdentity(Identity $identity): self
205
    {
206
        $attr_sync = $identity->getAdapter()->getAttributeSyncCache();
207
        if ($attr_sync === -1) {
208
            return $this;
209
        }
210
211
        $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...
212
            $this->last_attr_sync->toDateTime()->format('U') : 0);
213
214
        if (time() - $attr_sync > $cache) {
215
            $this->logger->info('user attribute sync cache time expired, resync with auth attributes', [
216
                'category' => get_class($this),
217
            ]);
218
219
            $attributes = $identity->getAttributes();
220
            foreach ($attributes as $attr => $value) {
221
                $this->{$attr} = $value;
222
            }
223
224
            $this->last_attr_sync = new UTCDateTime();
225
226
            $save = array_keys($attributes);
227
            $save[] = 'last_attr_sync';
228
229
            $this->save($save);
230
231
            return $this;
232
        }
233
234
        $this->logger->debug('user auth attribute sync cache is in time', [
235
            'category' => get_class($this),
236
        ]);
237
238
        return $this;
239
    }
240
241
    /**
242
     * Set user attributes.
243
     *
244
     * @param array $attributes
245
     *
246
     * @return bool
247
     */
248
    public function setAttributes(array $attributes = []): bool
249
    {
250
        $attributes = $this->server->validateUserAttributes($attributes);
251
252
        foreach ($attributes as $attr => $value) {
253
            $this->{$attr} = $value;
254
        }
255
256
        return $this->save(array_keys($attributes));
257
    }
258
259
    /**
260
     * Get Attributes.
261
     *
262
     * @return array
263
     */
264
    public function getAttributes(): array
265
    {
266
        return [
267
            '_id' => $this->_id,
268
            'username' => $this->username,
269
            'locale' => $this->locale,
270
            'namespace' => $this->namespace,
271
            'created' => $this->created,
272
            'changed' => $this->changed,
273
            'deleted' => $this->deleted,
274
            'soft_quota' => $this->soft_quota,
275
            'hard_quota' => $this->hard_quota,
276
            'mail' => $this->mail,
277
            'optional' => $this->optional,
278
            'avatar' => $this->avatar,
279
        ];
280
    }
281
282
    /**
283
     * Find all shares with membership.
284
     *
285
     * @return array
286
     */
287
    public function getShares(): array
288
    {
289
        $result = $this->getFilesystem()->findNodesByFilter([
290
            'deleted' => false,
291
            'shared' => true,
292
            'owner' => $this->_id,
293
        ]);
294
295
        $list = [];
296
        foreach ($result as $node) {
297
            if ($node->isReference()) {
298
                $list[] = $node->getShareId(true);
299
            } else {
300
                $list[] = $node->getId();
301
            }
302
        }
303
304
        return $list;
305
    }
306
307
    /**
308
     * Get node attribute usage.
309
     *
310
     * @param array|string $attributes
311
     * @param int          $limit
312
     *
313
     * @return array
314
     */
315
    public function getNodeAttributeSummary($attributes = [], int $limit = 25): array
316
    {
317
        $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...
318
319
        $valid = [
320
            'mime' => 'string',
321
            'meta.tags' => 'array',
322
            'meta.author' => 'string',
323
            'meta.color' => 'string',
324
            'meta.license' => 'string',
325
            'meta.copyright' => 'string',
326
        ];
327
328
        if (empty($attributes)) {
329
            $attributes = array_keys($valid);
330
        } elseif (is_string($attributes)) {
331
            $attributes = [$attributes];
332
        }
333
334
        $filter = array_intersect_key($valid, array_flip($attributes));
335
        $result = [];
336
337
        foreach ($filter as $attribute => $type) {
338
            $result[$attribute] = $this->_getAttributeSummary($attribute, $type, $limit);
339
        }
340
341
        return $result;
342
    }
343
344
    /**
345
     * Get filesystem.
346
     *
347
     * @return Filesystem
348
     */
349
    public function getFilesystem(): Filesystem
350
    {
351
        if ($this->fs instanceof Filesystem) {
352
            return $this->fs;
353
        }
354
355
        return $this->fs = $this->server->getFilesystem($this);
356
    }
357
358
    /**
359
     * Is Admin user?
360
     *
361
     * @return bool
362
     */
363
    public function isAdmin(): bool
364
    {
365
        return $this->admin;
366
    }
367
368
    /**
369
     * Check if user has share.
370
     *
371
     * @param Collection $node
372
     *
373
     * @return bool
374
     */
375
    public function hasShare(Collection $node): bool
376
    {
377
        $result = $this->db->storage->count([
378
            'reference' => $node->getId(),
379
            'directory' => true,
380
            'owner' => $this->_id,
381
        ]);
382
383
        return 1 === $result;
384
    }
385
386
    /**
387
     * Find new shares and create reference.
388
     *
389
     * @return User
390
     */
391
    public function updateShares(): self
392
    {
393
        $item = $this->db->storage->find([
394
            'deleted' => false,
395
            'shared' => true,
396
            'directory' => true,
397
            '$or' => [
398
                ['acl' => [
399
                    '$elemMatch' => [
400
                        'id' => (string) $this->_id,
401
                        'type' => 'user',
402
                    ],
403
                ]],
404
                ['acl' => [
405
                    '$elemMatch' => [
406
                        'id' => ['$in' => array_map('strval', $this->groups)],
407
                        'type' => 'group',
408
                    ],
409
                ]],
410
            ],
411
        ]);
412
413
        $found = [];
414
        $list = [];
415
        foreach ($item as $child) {
416
            $found[] = $child['_id'];
417
            $list[(string) $child['_id']] = $child;
418
        }
419
420
        if (empty($found)) {
421
            return $this;
422
        }
423
424
        //check for references
425
        $item = $this->db->storage->find([
426
            'directory' => true,
427
            'shared' => true,
428
            'owner' => $this->_id,
429
            'reference' => ['$exists' => 1],
430
        ]);
431
432
        $exists = [];
433
        foreach ($item as $child) {
434
            if (!in_array($child['reference'], $found)) {
435
                $this->logger->debug('found dead reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
436
                    'category' => get_class($this),
437
                ]);
438
439
                try {
440
                    $this->getFilesystem()->findNodeById($child['_id'])->delete(true);
441
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
442
                }
443
            } else {
444
                $this->logger->debug('found existing share reference ['.$child['_id'].'] pointing to share ['.$child['reference'].']', [
445
                    'category' => get_class($this),
446
                ]);
447
448
                $exists[] = $child['reference'];
449
            }
450
        }
451
452
        $new = array_diff($found, $exists);
453
        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...
454
            $node = $list[(string) $add];
455
456
            $this->logger->info('found new share ['.$node['_id'].']', [
457
                'category' => get_class($this),
458
            ]);
459
460
            if ($node['owner'] == $this->_id) {
461
                $this->logger->debug('skip creating reference to share ['.$node['_id'].'] cause share owner ['.$node['owner'].'] is the current user', [
462
                    'category' => get_class($this),
463
                ]);
464
465
                continue;
466
            }
467
468
            $attrs = [
469
                'shared' => true,
470
                'parent' => null,
471
                'reference' => $node['_id'],
472
            ];
473
474
            $dir = $this->getFilesystem()->getRoot();
475
476
            try {
477
                $dir->addDirectory($node['share_name'], $attrs);
478
            } catch (Exception\Conflict $e) {
479
                $conflict_node = $dir->getChild($node['share_name']);
480
481
                if (!$conflict_node->isReference() && $conflict_node->getShareId() != $attrs['reference']) {
482
                    $new = $node['share_name'].' ('.substr(uniqid('', true), -4).')';
483
                    $dir->addDirectory($new, $attrs);
484
                }
485
            } catch (\Exception $e) {
486
                $this->logger->error('failed create new share reference to share ['.$node['_id'].']', [
487
                    'category' => get_class($this),
488
                    'exception' => $e,
489
                ]);
490
491
                throw $e;
492
            }
493
494
            $this->logger->info('created new share reference to share ['.$node['_id'].']', [
495
                'category' => get_class($this),
496
            ]);
497
        }
498
499
        return $this;
500
    }
501
502
    /**
503
     * Get unique id.
504
     *
505
     * @return ObjectId
506
     */
507
    public function getId(): ObjectId
508
    {
509
        return $this->_id;
510
    }
511
512
    /**
513
     * Get namespace.
514
     *
515
     * @return string
516
     */
517
    public function getNamespace(): ?string
518
    {
519
        return $this->namespace;
520
    }
521
522
    /**
523
     * Get hard quota.
524
     *
525
     * @return int
526
     */
527
    public function getHardQuota(): int
528
    {
529
        return $this->hard_quota;
530
    }
531
532
    /**
533
     * Set hard quota.
534
     *
535
     * @param int $quota In Bytes
536
     *
537
     * @return User
538
     */
539
    public function setHardQuota(int $quota): self
540
    {
541
        $this->hard_quota = (int) $quota;
542
        $this->save(['hard_quota']);
543
544
        return $this;
545
    }
546
547
    /**
548
     * Set soft quota.
549
     *
550
     * @param int $quota In Bytes
551
     *
552
     * @return User
553
     */
554
    public function setSoftQuota(int $quota): self
555
    {
556
        $this->soft_quota = (int) $quota;
557
        $this->save(['soft_quota']);
558
559
        return $this;
560
    }
561
562
    /**
563
     * Save.
564
     *
565
     * @param array $attributes
566
     *
567
     * @return bool
568
     */
569
    public function save(array $attributes = []): bool
570
    {
571
        $this->changed = new UTCDateTime();
572
        $attributes[] = 'changed';
573
574
        $set = [];
575
        foreach ($attributes as $attr) {
576
            $set[$attr] = $this->{$attr};
577
        }
578
579
        $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...
580
            '_id' => $this->_id,
581
        ], [
582
            '$set' => $set,
583
        ]);
584
585
        return true;
586
    }
587
588
    /**
589
     * Get used qota.
590
     *
591
     * @return array
592
     */
593
    public function getQuotaUsage(): array
594
    {
595
        $result = $this->db->storage->find(
596
            [
597
                'owner' => $this->_id,
598
                'directory' => false,
599
                'deleted' => false,
600
            ],
601
            ['size']
602
        );
603
604
        $sum = 0;
605
        foreach ($result as $size) {
606
            if (isset($size['size'])) {
607
                $sum += $size['size'];
608
            }
609
        }
610
611
        return [
612
            'used' => $sum,
613
            'available' => ($this->hard_quota - $sum),
614
            'hard_quota' => $this->hard_quota,
615
            'soft_quota' => $this->soft_quota,
616
        ];
617
    }
618
619
    /**
620
     * Check quota.
621
     *
622
     * @param int $add Size in bytes
623
     *
624
     * @return bool
625
     */
626
    public function checkQuota(int $add): bool
627
    {
628
        if ($this->hard_quota === -1) {
629
            return true;
630
        }
631
632
        $quota = $this->getQuotaUsage();
633
634
        if (($quota['used'] + $add) > $quota['hard_quota']) {
635
            return false;
636
        }
637
638
        return true;
639
    }
640
641
    /**
642
     * Delete user.
643
     *
644
     * @param bool $force
645
     *
646
     * @return bool
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
     * Undelete user.
670
     *
671
     * @return bool
672
     */
673
    public function undelete(): bool
674
    {
675
        $this->deleted = false;
676
677
        return $this->save(['deleted']);
678
    }
679
680
    /**
681
     * Check if user is deleted.
682
     *
683
     * @return bool
684
     */
685
    public function isDeleted(): bool
686
    {
687
        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...
688
    }
689
690
    /**
691
     * Get Username.
692
     *
693
     * @return string
694
     */
695
    public function getUsername(): string
696
    {
697
        return $this->username;
698
    }
699
700
    /**
701
     * Get groups.
702
     *
703
     * @return array
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
     * @param string $attribute
726
     * @param string $type
727
     * @param int    $limit
728
     *
729
     * @return array
730
     */
731
    protected function _getAttributeSummary(string $attribute, string $type = 'string', int $limit = 25): array
732
    {
733
        $mongodb = $this->db->storage;
734
735
        $ops = [
736
            [
737
                '$match' => [
738
                    '$and' => [
739
                        ['owner' => $this->_id],
740
                        ['deleted' => false],
741
                        [$attribute => ['$exists' => true]],
742
                    ],
743
                ],
744
            ],
745
        ];
746
747
        if ('array' === $type) {
748
            $ops[] = [
749
                '$unwind' => '$'.$attribute,
750
            ];
751
        }
752
753
        $ops[] = [
754
            '$group' => [
755
                '_id' => '$'.$attribute,
756
                'sum' => ['$sum' => 1],
757
            ],
758
        ];
759
760
        $ops[] = [
761
            '$sort' => [
762
               'sum' => -1,
763
               '_id' => 1,
764
            ],
765
        ];
766
767
        $ops[] = [
768
            '$limit' => $limit,
769
        ];
770
771
        return $mongodb->aggregate($ops)->toArray();
772
    }
773
}
774