Completed
Branch dev (d5d70c)
by Raffael
11:00
created

Server   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 640
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 80
lcom 1
dl 0
loc 640
rs 4.7089
c 0
b 0
f 0
cbo 10

23 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 60 16
C validateUserAttributes() 0 64 19
A addUser() 0 15 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 getUsers() 0 14 3
A getGroups() 0 8 2
A getGroupByName() 0 12 2
A getGroupById() 0 12 2
A addGroup() 0 16 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\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use Balloon\Server\Group;
18
use Balloon\Server\Group\Exception as GroupException;
19
use Balloon\Server\User;
20
use Balloon\Server\User\Exception as UserException;
21
use Generator;
22
use Micro\Auth\Identity;
23
use MongoDB\BSON\Binary;
24
use MongoDB\BSON\ObjectId;
25
use MongoDB\BSON\UTCDateTime;
26
use MongoDB\Database;
27
use Psr\Log\LoggerInterface;
28
29
class Server
30
{
31
    /**
32
     * Database.
33
     *
34
     * @var Database
35
     */
36
    protected $db;
37
38
    /**
39
     * Storage.
40
     *
41
     * @var Storage
42
     */
43
    protected $storage;
44
45
    /**
46
     * LoggerInterface.
47
     *
48
     * @var LoggerInterface
49
     */
50
    protected $logger;
51
52
    /**
53
     * Hook.
54
     *
55
     * @var Hook
56
     */
57
    protected $hook;
58
59
    /**
60
     * Authenticated identity.
61
     *
62
     * @var User
63
     */
64
    protected $identity;
65
66
    /**
67
     * Acl.
68
     *
69
     * @var Acl
70
     */
71
    protected $acl;
72
73
    /**
74
     * Temporary store.
75
     *
76
     * @var string
77
     */
78
    protected $temp_dir = '/tmp/balloon';
79
80
    /**
81
     * Max file version.
82
     *
83
     * @var int
84
     */
85
    protected $max_file_version = 8;
86
87
    /**
88
     * Max file size.
89
     *
90
     * @var int
91
     */
92
    protected $max_file_size = 17179869184;
93
94
    /**
95
     * Password policy.
96
     *
97
     * @var string
98
     */
99
    protected $password_policy = '/.*/';
100
101
    /**
102
     * Password hash.
103
     *
104
     * @var int
105
     */
106
    protected $password_hash = PASSWORD_DEFAULT;
107
108
    /**
109
     * Server url.
110
     *
111
     * @var string
112
     */
113
    protected $server_url = 'https://localhost';
114
115
    /**
116
     * Initialize.
117
     *
118
     * @param Database        $db
119
     * @param Storage         $storage
120
     * @param LoggerInterface $logger
121
     * @param Hook            $hook
122
     * @param Acl             $acl
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 '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...
152
                case 'password_policy':
153
                case 'server_url':
154
                    $this->{$name} = (string) $value;
155
156
                break;
157
                case 'max_file_version':
158
                case 'max_file_size':
159
                case 'password_hash':
160
                    $this->{$name} = (int) $value;
161
162
                break;
163
                default:
164
                    throw new Exception('invalid option '.$name.' given');
165
            }
166
        }
167
168
        return $this;
169
    }
170
171
    /**
172
     * Get server url.
173
     *
174
     * @return string
175
     */
176
    public function getServerUrl(): string
177
    {
178
        return $this->server_url;
179
    }
180
181
    /**
182
     * Get temporary directory.
183
     *
184
     * @return string
185
     */
186
    public function getTempDir(): string
187
    {
188
        return $this->temp_dir;
189
    }
190
191
    /**
192
     * Get max file version.
193
     *
194
     * @return int
195
     */
196
    public function getMaxFileVersion(): int
197
    {
198
        return $this->max_file_version;
199
    }
200
201
    /**
202
     * Get max file size.
203
     *
204
     * @return int
205
     */
206
    public function getMaxFileSize(): int
207
    {
208
        return $this->max_file_size;
209
    }
210
211
    /**
212
     * Filesystem factory.
213
     *
214
     * @return Filesystem
215
     */
216
    public function getFilesystem(?User $user = null): Filesystem
217
    {
218
        if (null !== $user) {
219
            return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl, $user);
220
        }
221
        if ($this->identity instanceof User) {
222
            return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl, $this->identity);
223
        }
224
225
        return new Filesystem($this, $this->db, $this->hook, $this->logger, $this->storage, $this->acl);
226
    }
227
228
    /**
229
     * Verify group attributes.
230
     *
231
     * @param array $attributes
232
     *
233
     * @return array
234
     */
235
    public function validateGroupAttributes(array $attributes): array
236
    {
237
        foreach ($attributes as $attribute => &$value) {
238
            switch ($attribute) {
239
                case 'namespace':
240
                case 'description':
241
                    if (!is_string($value)) {
242
                        throw new GroupException($attribute.' must be a valid string');
243
                    }
244
245
                break;
246
                case 'name':
247
                    if (!is_string($value)) {
248
                        throw new GroupException($attribute.' must be a valid string');
249
                    }
250
251
                    if ($this->groupExists($value)) {
252
                        throw new GroupException('group does already exists', GroupException::ALREADY_EXISTS);
253
                    }
254
255
                break;
256
                case 'optional':
257
                    if (is_array($value)) {
258
                        throw new GroupException('optional group attributes must be an array');
259
                    }
260
261
                break;
262
                case 'member':
263
                    if (!is_array($value)) {
264
                        throw new GroupException('member must be an array of user');
265
                    }
266
267
                    $valid = [];
268
                    foreach ($value as $id) {
269
                        if ($id instanceof User) {
270
                            $id = $id->getId();
271
                        } else {
272
                            $id = new ObjectId($id);
273
                            if (!$this->userExists($id)) {
274
                                throw new UserException('user does not exists', UserException::DOES_NOT_EXISTS);
275
                            }
276
                        }
277
278
                        if (in_array($id, $valid)) {
279
                            throw new GroupException('group can only hold a user once', GroupException::UNIQUE_MEMBER);
280
                        }
281
282
                        $valid[] = $id;
283
                    }
284
285
                    $value = $valid;
286
287
                break;
288
                default:
289
                    throw new GroupException('invalid attribute '.$attribute.' given');
290
            }
291
        }
292
293
        return $attributes;
294
    }
295
296
    /**
297
     * Verify user attributes.
298
     *
299
     * @param array $attributes
300
     *
301
     * @return array
302
     */
303
    public function validateUserAttributes(array $attributes): array
304
    {
305
        foreach ($attributes as $attribute => &$value) {
306
            switch ($attribute) {
307
                case 'username':
308
                    if (!preg_match('/^[A-Za-z0-9\.-_\@]+$/', $value)) {
309
                        throw new UserException('username does not match required regex /^[A-Za-z0-9\.-_\@]+$/');
310
                    }
311
312
                    if ($this->userExists($value)) {
313
                        throw new UserException('user does already exists', UserException::ALREADY_EXISTS);
314
                    }
315
316
                break;
317
                case 'password':
318
                    if (!preg_match($this->password_policy, $value)) {
319
                        throw new UserException('password does not follow password policy '.$this->password_policy);
320
                    }
321
322
                    $value = password_hash($value, $this->password_hash);
323
324
                break;
325
                case 'soft_quota':
326
                case 'hard_quota':
327
                    if (!is_numeric($value)) {
328
                        throw new UserException($attribute.' must be numeric');
329
                    }
330
331
                break;
332
                case 'avatar':
333
                    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...
334
                        throw new UserException('avatar must be an instance of Binary');
335
                    }
336
337
                break;
338
                case 'mail':
339
                    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
340
                        throw new UserException('mail address given is invalid');
341
                    }
342
343
                break;
344
                case 'admin':
345
                    $value = (bool) $value;
346
347
                break;
348
                case 'namespace':
349
                    if (!is_string($value)) {
350
                        throw new UserException('namespace must be a valid string');
351
                    }
352
353
                    // no break
354
                case 'optional':
355
                    if (is_array($value)) {
356
                        throw new UserException('optional user attributes must be an array');
357
                    }
358
359
                break;
360
                default:
361
                    throw new UserException('invalid attribute '.$attribute.' given');
362
            }
363
        }
364
365
        return $attributes;
366
    }
367
368
    /**
369
     * Add user.
370
     *
371
     * @param string $username
372
     * @param array  $attributes
373
     *
374
     * @return ObjectId
375
     */
376
    public function addUser(string $username, array $attributes = []): ObjectId
377
    {
378
        $attributes['username'] = $username;
379
        $attributes = $this->validateUserAttributes($attributes);
380
381
        $defaults = [
382
            'created' => new UTCDateTime(),
383
            'deleted' => false,
384
        ];
385
386
        $attributes = array_merge($defaults, $attributes);
387
        $result = $this->db->user->insertOne($attributes);
388
389
        return $result->getInsertedId();
390
    }
391
392
    /**
393
     * Check if user exists.
394
     *
395
     * @return bool
396
     */
397
    public function userExists(string $username): bool
398
    {
399
        return  1 === $this->db->user->count(['username' => $username]);
400
    }
401
402
    /**
403
     * Check if group exists.
404
     *
405
     * @return bool
406
     */
407
    public function groupExists(string $name): bool
408
    {
409
        return  1 === $this->db->group->count(['name' => $name]);
410
    }
411
412
    /**
413
     * Get user by id.
414
     *
415
     * @param ObjectId $id
416
     *
417
     * @return User
418
     */
419
    public function getUserById(ObjectId $id): User
420
    {
421
        $aggregation = $this->getUserAggregationPipes();
422
        array_unshift($aggregation, ['$match' => ['_id' => $id]]);
423
        $users = $this->db->user->aggregate($aggregation)->toArray();
424
425
        if (count($users) > 1) {
426
            throw new UserException('multiple user found', UserException::MULTIPLE_USER_FOUND);
427
        }
428
429
        if (count($users) === 0) {
430
            throw new UserException('user does not exists', UserException::DOES_NOT_EXISTS);
431
        }
432
433
        return new User(array_shift($users), $this, $this->db, $this->logger);
434
    }
435
436
    /**
437
     * Get users by id.
438
     *
439
     * @param array $id
440
     *
441
     * @return Generator
442
     */
443
    public function getUsersById(array $id): Generator
444
    {
445
        $find = [];
446
        foreach ($id as $i) {
447
            $find[] = new ObjectId($i);
448
        }
449
450
        $filter = [
451
            '$match' => [
452
                '_id' => ['$in' => $find],
453
            ],
454
        ];
455
456
        $aggregation = $this->getUserAggregationPipes();
457
        array_unshift($aggregation, $filter);
458
        $users = $this->db->user->aggregate($aggregation);
459
460
        foreach ($users as $attributes) {
461
            yield new User($attributes, $this, $this->db, $this->logger);
462
        }
463
    }
464
465
    /**
466
     * Set Identity.
467
     *
468
     * @param Identity $identity
469
     *
470
     * @return bool
471
     */
472
    public function setIdentity(Identity $identity): bool
473
    {
474
        $user = null;
475
476
        try {
477
            $user = $this->getUserByName($identity->getIdentifier());
478
        } catch (UserException $e) {
479
            $this->logger->warning('failed connect authenticated user, user account does not exists', [
480
                'category' => get_class($this),
481
            ]);
482
        }
483
484
        $this->hook->run('preServerIdentity', [$identity, &$user]);
485
486
        if (!($user instanceof User)) {
487
            throw new Exception\NotAuthenticated('user does not exists', Exception\NotAuthenticated::USER_DOES_NOT_EXISTS);
488
        }
489
490
        if ($user->isDeleted()) {
491
            throw new Exception\NotAuthenticated(
492
                'user is disabled and can not be used',
493
                Exception\NotAuthenticated::USER_DELETED
494
            );
495
        }
496
497
        $this->identity = $user;
498
        $user->updateIdentity($identity)
499
             ->updateShares();
500
        $this->hook->run('postServerIdentity', [$user]);
501
502
        return true;
503
    }
504
505
    /**
506
     * Get authenticated user.
507
     *
508
     * @return User
509
     */
510
    public function getIdentity(): ?User
511
    {
512
        return $this->identity;
513
    }
514
515
    /**
516
     * Get user by name.
517
     *
518
     * @param string $name
519
     *
520
     * @return User
521
     */
522
    public function getUserByName(string $name): User
523
    {
524
        $aggregation = $this->getUserAggregationPipes();
525
        array_unshift($aggregation, ['$match' => ['username' => $name]]);
526
        $users = $this->db->user->aggregate($aggregation)->toArray();
527
528
        if (count($users) > 1) {
529
            throw new UserException('multiple user found', UserException::MULTIPLE_USER_FOUND);
530
        }
531
532
        if (count($users) === 0) {
533
            throw new UserException('user does not exists', UserException::DOES_NOT_EXISTS);
534
        }
535
536
        return new User(array_shift($users), $this, $this->db, $this->logger);
537
    }
538
539
    /**
540
     * Get users.
541
     *
542
     * @param array $filter
543
     *
544
     * @return Generator
545
     */
546
    public function getUsers(array $filter): Generator
547
    {
548
        $aggregation = $this->getUserAggregationPipes();
549
550
        if (count($filter) > 0) {
551
            array_unshift($aggregation, ['$match' => $filter]);
552
        }
553
554
        $users = $this->db->user->aggregate($aggregation);
555
556
        foreach ($users as $attributes) {
557
            yield new User($attributes, $this, $this->db, $this->logger);
558
        }
559
    }
560
561
    /**
562
     * Get groups.
563
     *
564
     * @param array $filter
565
     *
566
     * @return Generator
567
     */
568
    public function getGroups(array $filter): Generator
569
    {
570
        $groups = $this->db->group->find($filter);
571
572
        foreach ($groups as $attributes) {
573
            yield new Group($attributes, $this, $this->db, $this->logger);
574
        }
575
    }
576
577
    /**
578
     * Get group by name.
579
     *
580
     * @param string $name
581
     *
582
     * @return Group
583
     */
584
    public function getGroupByName(string $name): Group
585
    {
586
        $attributes = $this->db->group->findOne([
587
           'name' => $name,
588
        ]);
589
590
        if (null === $attributes) {
591
            throw new GroupException('group does not exists', GroupException::DOES_NOT_EXISTS);
592
        }
593
594
        return new Group($attributes, $this, $this->db, $this->logger);
595
    }
596
597
    /**
598
     * Get group by id.
599
     *
600
     * @param string $id
601
     *
602
     * @return User
603
     */
604
    public function getGroupById(ObjectId $id): Group
605
    {
606
        $attributes = $this->db->group->findOne([
607
           '_id' => $id,
608
        ]);
609
610
        if (null === $attributes) {
611
            throw new GroupException('group does not exists', GroupException::DOES_NOT_EXISTS);
612
        }
613
614
        return new Group($attributes, $this, $this->db, $this->logger);
615
    }
616
617
    /**
618
     * Add group.
619
     *
620
     * @param string $name
621
     * @param array  $member
622
     * @param array  $attributes
623
     *
624
     * @return ObjectId
625
     */
626
    public function addGroup(string $name, array $member = [], array $attributes = []): ObjectId
627
    {
628
        $attributes['member'] = $member;
629
        $attributes['name'] = $name;
630
        $attributes = $this->validateGroupAttributes($attributes);
631
632
        $defaults = [
633
            'created' => new UTCDateTime(),
634
            'deleted' => false,
635
        ];
636
637
        $attributes = array_merge($attributes, $defaults);
638
        $result = $this->db->group->insertOne($attributes);
639
640
        return $result->getInsertedId();
641
    }
642
643
    /**
644
     * Get user aggregation pipe.
645
     *
646
     * @return array
647
     */
648
    protected function getUserAggregationPipes(): array
649
    {
650
        return [
651
            ['$lookup' => [
652
                'from' => 'group',
653
                'localField' => '_id',
654
                'foreignField' => 'member',
655
                'as' => 'groups',
656
            ]],
657
            ['$addFields' => [
658
                'groups' => [
659
                    '$map' => [
660
                        'input' => '$groups',
661
                        'as' => 'group',
662
                        'in' => '$$group._id',
663
                    ],
664
                ],
665
            ]],
666
        ];
667
    }
668
}
669