Completed
Pull Request — master (#141)
by Raffael
11:16
created

Server   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 682
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 87
lcom 1
cbo 13
dl 0
loc 682
ccs 0
cts 257
cp 0
rs 1.918
c 0
b 0
f 0

26 Methods

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