Completed
Branch master (be8900)
by Raffael
09:55 queued 05:31
created

Server   D

Complexity

Total Complexity 85

Size/Duplication

Total Lines 736
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 85
cbo 10
dl 0
loc 736
ccs 0
cts 248
cp 0
rs 4.4444
c 0
b 0
f 0
lcom 1

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
D setOptions() 0 27 9
A getServerUrl() 0 4 1
A getTempDir() 0 4 1
A getMaxFileVersion() 0 4 1
A getMaxFileSize() 0 4 1
A getFilesystem() 0 11 3
C validateGroupAttributes() 0 68 14
D validateUserAttributes() 0 97 21
A addUser() 0 16 1
A usernameExists() 0 4 1
A userExists() 0 4 1
A groupExists() 0 4 1
A getUserById() 0 16 3
A getUsersById() 0 21 3
B setIdentity() 0 32 4
A getIdentity() 0 4 1
A getUserByName() 0 16 3
A countUsers() 0 4 1
A countGroups() 0 4 1
B getUsers() 0 24 5
A getGroups() 0 13 2
A getGroupByName() 0 12 2
A getGroupById() 0 12 2
A addGroup() 0 17 1
A getUserAggregationPipes() 0 20 1

How to fix   Complexity   

Complex Class

Complex classes like Server 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 Server, 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;
13
14
use Balloon\Filesystem\Acl;
15
use Balloon\Filesystem\Storage;
16
use Balloon\Server\Group;
17
use Balloon\Server\User;
18
use Generator;
19
use InvalidArgumentException;
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 Psr\Log\LoggerInterface;
26
27
class Server
28
{
29
    /**
30
     * Database.
31
     *
32
     * @var Database
33
     */
34
    protected $db;
35
36
    /**
37
     * Storage.
38
     *
39
     * @var Storage
40
     */
41
    protected $storage;
42
43
    /**
44
     * LoggerInterface.
45
     *
46
     * @var LoggerInterface
47
     */
48
    protected $logger;
49
50
    /**
51
     * Hook.
52
     *
53
     * @var Hook
54
     */
55
    protected $hook;
56
57
    /**
58
     * Authenticated identity.
59
     *
60
     * @var User
61
     */
62
    protected $identity;
63
64
    /**
65
     * Acl.
66
     *
67
     * @var Acl
68
     */
69
    protected $acl;
70
71
    /**
72
     * Temporary store.
73
     *
74
     * @var string
75
     */
76
    protected $temp_dir = '/tmp/balloon';
77
78
    /**
79
     * Max file version.
80
     *
81
     * @var int
82
     */
83
    protected $max_file_version = 8;
84
85
    /**
86
     * Max file size.
87
     *
88
     * @var int
89
     */
90
    protected $max_file_size = 17179869184;
91
92
    /**
93
     * Password policy.
94
     *
95
     * @var string
96
     */
97
    protected $password_policy = '/.*/';
98
99
    /**
100
     * Password hash.
101
     *
102
     * @var int
103
     */
104
    protected $password_hash = PASSWORD_DEFAULT;
105
106
    /**
107
     * Server url.
108
     *
109
     * @var string
110
     */
111
    protected $server_url = 'https://localhost';
112
113
    /**
114
     * Initialize.
115
     *
116
     * @param Database        $db
117
     * @param Storage         $storage
118
     * @param LoggerInterface $logger
119
     * @param Hook            $hook
120
     * @param Acl             $acl
121
     * @param iterable        $config
122
     */
123
    public function __construct(Database $db, Storage $storage, LoggerInterface $logger, Hook $hook, Acl $acl, ?Iterable $config = null)
124
    {
125
        $this->db = $db;
126
        $this->storage = $storage;
127
        $this->logger = $logger;
128
        $this->hook = $hook;
129
        $this->acl = $acl;
130
131
        $this->setOptions($config);
132
    }
133
134
    /**
135
     * Set options.
136
     *
137
     * @param iterable $config
138
     *
139
     * @return Server
140
     */
141
    public function setOptions(?Iterable $config = null): self
142
    {
143
        if (null === $config) {
144
            return $this;
145
        }
146
147
        foreach ($config as $name => $value) {
148
            switch ($name) {
149
                case 'temp_dir':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
150
                case 'password_policy':
151
                case 'server_url':
152
                    $this->{$name} = (string) $value;
153
154
                break;
155
                case 'max_file_version':
156
                case 'max_file_size':
157
                case 'password_hash':
158
                    $this->{$name} = (int) $value;
159
160
                break;
161
                default:
162
                    throw new InvalidArgumentException('invalid option '.$name.' given');
163
            }
164
        }
165
166
        return $this;
167
    }
168
169
    /**
170
     * Get server url.
171
     *
172
     * @return string
173
     */
174
    public function getServerUrl(): string
175
    {
176
        return $this->server_url;
177
    }
178
179
    /**
180
     * Get temporary directory.
181
     *
182
     * @return string
183
     */
184
    public function getTempDir(): string
185
    {
186
        return $this->temp_dir;
187
    }
188
189
    /**
190
     * Get max file version.
191
     *
192
     * @return int
193
     */
194
    public function getMaxFileVersion(): int
195
    {
196
        return $this->max_file_version;
197
    }
198
199
    /**
200
     * Get max file size.
201
     *
202
     * @return int
203
     */
204
    public function getMaxFileSize(): int
205
    {
206
        return $this->max_file_size;
207
    }
208
209
    /**
210
     * Filesystem factory.
211
     *
212
     * @return Filesystem
213
     */
214
    public function getFilesystem(?User $user = null): Filesystem
215
    {
216
        if (null !== $user) {
217
            return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl, $user);
218
        }
219
        if ($this->identity instanceof User) {
220
            return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl, $this->identity);
221
        }
222
223
        return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl);
224
    }
225
226
    /**
227
     * Verify group attributes.
228
     *
229
     * @param array $attributes
230
     *
231
     * @return array
232
     */
233
    public function validateGroupAttributes(array $attributes): array
234
    {
235
        foreach ($attributes as $attribute => &$value) {
236
            switch ($attribute) {
237
                case 'namespace':
238
                    if (!is_string($value)) {
239
                        throw new Group\Exception\InvalidArgument(
240
                            $attribute.' must be a valid string',
241
                            Group\Exception\InvalidArgument::INVALID_NAMESPACE
242
                        );
243
                    }
244
245
                break;
246
                case 'name':
247
                    if (!is_string($value)) {
248
                        throw new Group\Exception\InvalidArgument(
249
                            $attribute.' must be a valid string',
250
                            Group\Exception\InvalidArgument::INVALID_NAME
251
                        );
252
                    }
253
254
                break;
255
                case 'optional':
256
                    if (!is_array($value)) {
257
                        throw new Group\Exception\InvalidArgument(
258
                            'optional group attributes must be an array',
259
                            Group\Exception\InvalidArgument::INVALID_OPTIONAL
260
                        );
261
                    }
262
263
                break;
264
                case 'member':
265
                    if (!is_array($value)) {
266
                        throw new Group\Exception\InvalidArgument(
267
                            'member must be an array of user',
268
                            Group\Exception\InvalidArgument::INVALID_MEMBER
269
                        );
270
                    }
271
272
                    $valid = [];
273
                    foreach ($value as $id) {
274
                        if ($id instanceof User) {
275
                            $id = $id->getId();
276
                        } else {
277
                            $id = new ObjectId($id);
278
                            if (!$this->userExists($id)) {
279
                                throw new User\Exception\NotFound('user does not exists');
280
                            }
281
                        }
282
283
                        if (!in_array($id, $valid)) {
284
                            $valid[] = $id;
285
                        }
286
                    }
287
288
                    $value = $valid;
289
290
                break;
291
                default:
292
                    throw new Group\Exception\InvalidArgument(
293
                        'invalid attribute '.$attribute.' given',
294
                        Group\Exception\InvalidArgument::INVALID_ATTRIBUTE
295
                    );
296
            }
297
        }
298
299
        return $attributes;
300
    }
301
302
    /**
303
     * Verify user attributes.
304
     *
305
     * @param array $attributes
306
     *
307
     * @return array
308
     */
309
    public function validateUserAttributes(array $attributes): array
310
    {
311
        foreach ($attributes as $attribute => &$value) {
312
            switch ($attribute) {
313
                case 'username':
314
                    if (!preg_match('/^[A-Za-z0-9\.-_\@]+$/', $value)) {
315
                        throw new User\Exception\InvalidArgument(
316
                            'username does not match required regex /^[A-Za-z0-9\.-_\@]+$/',
317
                            User\Exception\InvalidArgument::INVALID_USERNAME
318
                        );
319
                    }
320
321
                    if ($this->usernameExists($value)) {
322
                        throw new User\Exception\NotUnique('user does already exists');
323
                    }
324
325
                break;
326
                case 'password':
327
                    if (!preg_match($this->password_policy, $value)) {
328
                        throw new User\Exception\InvalidArgument(
329
                            'password does not follow password policy '.$this->password_policy,
330
                            User\Exception\InvalidArgument::INVALID_PASSWORD
331
                        );
332
                    }
333
334
                    $value = password_hash($value, $this->password_hash);
335
336
                break;
337
                case 'soft_quota':
338
                case 'hard_quota':
339
                    if (!is_numeric($value)) {
340
                        throw new User\Exception\InvalidArgument(
341
                            $attribute.' must be numeric',
342
                            User\Exception\InvalidArgument::INVALID_QUOTA
343
                        );
344
                    }
345
346
                break;
347
                case 'avatar':
348
                    if (!$value instanceof Binary) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\Binary 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...
349
                        throw new User\Exception\InvalidArgument(
350
                            'avatar must be an instance of Binary',
351
                            User\Exception\InvalidArgument::INVALID_AVATAR
352
                        );
353
                    }
354
355
                break;
356
                case 'mail':
357
                    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
358
                        throw new User\Exception\InvalidArgument(
359
                            'mail address given is invalid',
360
                            User\Exception\InvalidArgument::INVALID_MAIL
361
                        );
362
                    }
363
364
                break;
365
                case 'admin':
366
                    $value = (bool) $value;
367
368
                break;
369
                case 'locale':
370
                    if (!preg_match('#^[a-z]{2}_[A-Z]{2}$#', $value)) {
371
                        throw new User\Exception\InvalidArgument(
372
                            'invalid locale given, must be according to format a-z_A-Z',
373
                            User\Exception\InvalidArgument::INVALID_LOCALE
374
                        );
375
                    }
376
377
                break;
378
                case 'namespace':
379
                    if (!is_string($value)) {
380
                        throw new User\Exception\InvalidArgument(
381
                            'namespace must be a valid string',
382
                            User\Exception\InvalidArgument::INVALID_NAMESPACE
383
                        );
384
                    }
385
386
                break;
387
                case 'optional':
388
                    if (!is_array($value)) {
389
                        throw new User\Exception\InvalidArgument(
390
                            'optional user attributes must be an array',
391
                            User\Exception\InvalidArgument::INVALID_OPTIONAL
392
                        );
393
                    }
394
395
                break;
396
                default:
397
                    throw new User\Exception\InvalidArgument(
398
                        'invalid attribute '.$attribute.' given',
399
                        User\Exception\InvalidArgument::INVALID_ATTRIBUTE
400
                    );
401
            }
402
        }
403
404
        return $attributes;
405
    }
406
407
    /**
408
     * Add user.
409
     *
410
     * @param string $username
411
     * @param array  $attributes
412
     *
413
     * @return ObjectId
414
     */
415
    public function addUser(string $username, array $attributes = []): ObjectId
416
    {
417
        $attributes['username'] = $username;
418
        $attributes = $this->validateUserAttributes($attributes);
419
420
        $defaults = [
421
            'created' => new UTCDateTime(),
422
            'changed' => new UTCDateTime(),
423
            'deleted' => false,
424
        ];
425
426
        $attributes = array_merge($defaults, $attributes);
427
        $result = $this->db->user->insertOne($attributes);
428
429
        return $result->getInsertedId();
430
    }
431
432
    /**
433
     * Check if user exists.
434
     *
435
     * @return bool
436
     */
437
    public function usernameExists(string $username): bool
438
    {
439
        return  1 === $this->db->user->count(['username' => $username]);
440
    }
441
442
    /**
443
     * Check if user exists.
444
     *
445
     * @return bool
446
     */
447
    public function userExists(ObjectId $id): bool
448
    {
449
        return  1 === $this->db->user->count(['_id' => $id]);
450
    }
451
452
    /**
453
     * Check if user exists.
454
     *
455
     * @return bool
456
     */
457
    public function groupExists(ObjectId $id): bool
458
    {
459
        return  1 === $this->db->group->count(['_id' => $id]);
460
    }
461
462
    /**
463
     * Get user by id.
464
     *
465
     * @param ObjectId $id
466
     *
467
     * @return User
468
     */
469
    public function getUserById(ObjectId $id): User
470
    {
471
        $aggregation = $this->getUserAggregationPipes();
472
        array_unshift($aggregation, ['$match' => ['_id' => $id]]);
473
        $users = $this->db->user->aggregate($aggregation)->toArray();
474
475
        if (count($users) > 1) {
476
            throw new User\Exception\NotUnique('multiple user found');
477
        }
478
479
        if (count($users) === 0) {
480
            throw new User\Exception\NotFound('user does not exists');
481
        }
482
483
        return new User(array_shift($users), $this, $this->db, $this->logger);
484
    }
485
486
    /**
487
     * Get users by id.
488
     *
489
     * @param array $id
490
     *
491
     * @return Generator
492
     */
493
    public function getUsersById(array $id): Generator
494
    {
495
        $find = [];
496
        foreach ($id as $i) {
497
            $find[] = new ObjectId($i);
498
        }
499
500
        $filter = [
501
            '$match' => [
502
                '_id' => ['$in' => $find],
503
            ],
504
        ];
505
506
        $aggregation = $this->getUserAggregationPipes();
507
        array_unshift($aggregation, $filter);
508
        $users = $this->db->user->aggregate($aggregation);
509
510
        foreach ($users as $attributes) {
511
            yield new User($attributes, $this, $this->db, $this->logger);
512
        }
513
    }
514
515
    /**
516
     * Set Identity.
517
     *
518
     * @param Identity $identity
519
     *
520
     * @return bool
521
     */
522
    public function setIdentity(Identity $identity): bool
523
    {
524
        $user = null;
525
526
        try {
527
            $user = $this->getUserByName($identity->getIdentifier());
528
        } catch (User\Exception\NotFound $e) {
529
            $this->logger->warning('failed connect authenticated user, user account does not exists', [
530
                'category' => get_class($this),
531
            ]);
532
        }
533
534
        $this->hook->run('preServerIdentity', [$identity, &$user]);
535
536
        if (!($user instanceof User)) {
537
            throw new User\Exception\NotAuthenticated('user does not exists', User\Exception\NotAuthenticated::USER_NOT_FOUND);
538
        }
539
540
        if ($user->isDeleted()) {
541
            throw new User\Exception\NotAuthenticated(
542
                'user is disabled and can not be used',
543
                User\Exception\NotAuthenticated::USER_DELETED
544
            );
545
        }
546
547
        $this->identity = $user;
548
        $user->updateIdentity($identity)
549
             ->updateShares();
550
        $this->hook->run('postServerIdentity', [$user]);
551
552
        return true;
553
    }
554
555
    /**
556
     * Get authenticated user.
557
     *
558
     * @return User
559
     */
560
    public function getIdentity(): ?User
561
    {
562
        return $this->identity;
563
    }
564
565
    /**
566
     * Get user by name.
567
     *
568
     * @param string $name
569
     *
570
     * @return User
571
     */
572
    public function getUserByName(string $name): User
573
    {
574
        $aggregation = $this->getUserAggregationPipes();
575
        array_unshift($aggregation, ['$match' => ['username' => $name]]);
576
        $users = $this->db->user->aggregate($aggregation)->toArray();
577
578
        if (count($users) > 1) {
579
            throw new User\Exception\NotUnique('multiple user found');
580
        }
581
582
        if (count($users) === 0) {
583
            throw new User\Exception\NotFound('user does not exists');
584
        }
585
586
        return new User(array_shift($users), $this, $this->db, $this->logger);
587
    }
588
589
    /**
590
     * Count users.
591
     *
592
     * @param array $filter
593
     *
594
     * @return int
595
     */
596
    public function countUsers(array $filter): int
597
    {
598
        return $this->db->user->count($filter);
599
    }
600
601
    /**
602
     * Count groups.
603
     *
604
     * @param array $filter
605
     *
606
     * @return int
607
     */
608
    public function countGroups(array $filter): int
609
    {
610
        return $this->db->group->count($filter);
611
    }
612
613
    /**
614
     * Get users.
615
     *
616
     * @param array $filter
617
     * @param int   $offset
618
     * @param int   $limit
619
     *
620
     * @return Generator
621
     */
622
    public function getUsers(array $filter, ?int $offset = null, ?int $limit = null): Generator
623
    {
624
        $aggregation = $this->getUserAggregationPipes();
625
626
        if (count($filter) > 0) {
627
            array_unshift($aggregation, ['$match' => $filter]);
628
        }
629
630
        if ($offset !== null) {
631
            array_unshift($aggregation, ['$skip' => $offset]);
632
        }
633
634
        if ($limit !== null) {
635
            $aggregation[] = ['$limit' => $limit];
636
        }
637
638
        $users = $this->db->user->aggregate($aggregation);
639
640
        foreach ($users as $attributes) {
641
            yield new User($attributes, $this, $this->db, $this->logger);
642
        }
643
644
        return $this->db->user->count($filter);
645
    }
646
647
    /**
648
     * Get groups.
649
     *
650
     * @param array $filter
651
     * @param int   $offset
652
     * @param int   $limit
653
     *
654
     * @return Generator
655
     */
656
    public function getGroups(array $filter, ?int $offset = null, ?int $limit = null): Generator
657
    {
658
        $groups = $this->db->group->find($filter, [
659
            'skip' => $offset,
660
            'limit' => $limit,
661
        ]);
662
663
        foreach ($groups as $attributes) {
664
            yield new Group($attributes, $this, $this->db, $this->logger);
665
        }
666
667
        return $this->db->group->count($filter);
668
    }
669
670
    /**
671
     * Get group by name.
672
     *
673
     * @param string $name
674
     *
675
     * @return Group
676
     */
677
    public function getGroupByName(string $name): Group
678
    {
679
        $group = $this->db->group->findOne([
680
           'name' => $name,
681
        ]);
682
683
        if (null === $group) {
684
            throw new Group\Exception\NotFound('group does not exists');
685
        }
686
687
        return new Group($group, $this, $this->db, $this->logger);
688
    }
689
690
    /**
691
     * Get group by id.
692
     *
693
     * @param string $id
694
     *
695
     * @return Group
696
     */
697
    public function getGroupById(ObjectId $id): Group
698
    {
699
        $group = $this->db->group->findOne([
700
           '_id' => $id,
701
        ]);
702
703
        if (null === $group) {
704
            throw new Group\Exception\NotFound('group does not exists');
705
        }
706
707
        return new Group($group, $this, $this->db, $this->logger);
708
    }
709
710
    /**
711
     * Add group.
712
     *
713
     * @param string $name
714
     * @param array  $member
715
     * @param array  $attributes
716
     *
717
     * @return ObjectId
718
     */
719
    public function addGroup(string $name, array $member = [], array $attributes = []): ObjectId
720
    {
721
        $attributes['member'] = $member;
722
        $attributes['name'] = $name;
723
        $attributes = $this->validateGroupAttributes($attributes);
724
725
        $defaults = [
726
            'created' => new UTCDateTime(),
727
            'changed' => new UTCDateTime(),
728
            'deleted' => false,
729
        ];
730
731
        $attributes = array_merge($attributes, $defaults);
732
        $result = $this->db->group->insertOne($attributes);
733
734
        return $result->getInsertedId();
735
    }
736
737
    /**
738
     * Get user aggregation pipe.
739
     *
740
     * @return array
741
     */
742
    protected function getUserAggregationPipes(): array
743
    {
744
        return [
745
            ['$lookup' => [
746
                'from' => 'group',
747
                'localField' => '_id',
748
                'foreignField' => 'member',
749
                'as' => 'groups',
750
            ]],
751
            ['$addFields' => [
752
                'groups' => [
753
                    '$map' => [
754
                        'input' => '$groups',
755
                        'as' => 'group',
756
                        'in' => '$$group._id',
757
                    ],
758
                ],
759
            ]],
760
        ];
761
    }
762
}
763