Completed
Branch dev (276354)
by Raffael
15:43
created

Server::getUsers()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.5125
c 0
b 0
f 0
cc 5
eloc 12
nc 16
nop 3
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
                    if ($this->groupExists($value)) {
255
                        throw new Group\Exception\NotUnique('group does already exists');
256
                    }
257
258
                break;
259
                case 'optional':
260
                    if (!is_array($value)) {
261
                        throw new Group\Exception\InvalidArgument(
262
                            'optional group attributes must be an array',
263
                            Group\Exception\InvalidArgument::INVALID_OPTIONAL
264
                        );
265
                    }
266
267
                break;
268
                case 'member':
269
                    if (!is_array($value)) {
270
                        throw new Group\Exception\InvalidArgument(
271
                            'member must be an array of user',
272
                            Group\Exception\InvalidArgument::INVALID_MEMBER
273
                        );
274
                    }
275
276
                    $valid = [];
277
                    foreach ($value as $id) {
278
                        if ($id instanceof User) {
279
                            $id = $id->getId();
280
                        } else {
281
                            $id = new ObjectId($id);
282
                            if (!$this->userExists($id)) {
283
                                throw new User\Exception\NotFound('user does not exists');
284
                            }
285
                        }
286
287
                        if (!in_array($id, $valid)) {
288
                            $valid[] = $id;
289
                        }
290
                    }
291
292
                    $value = $valid;
293
294
                break;
295
                default:
296
                    throw new Group\Exception\InvalidArgument(
297
                        'invalid attribute '.$attribute.' given',
298
                        Group\Exception\InvalidArgument::INVALID_ATTRIBUTE
299
                    );
300
            }
301
        }
302
303
        return $attributes;
304
    }
305
306
    /**
307
     * Verify user attributes.
308
     *
309
     * @param array $attributes
310
     *
311
     * @return array
312
     */
313
    public function validateUserAttributes(array $attributes): array
314
    {
315
        foreach ($attributes as $attribute => &$value) {
316
            switch ($attribute) {
317
                case 'username':
318
                    if (!preg_match('/^[A-Za-z0-9\.-_\@]+$/', $value)) {
319
                        throw new User\Exception\InvalidArgument(
320
                            'username does not match required regex /^[A-Za-z0-9\.-_\@]+$/',
321
                            User\Exception\InvalidArgument::INVALID_USERNAME
322
                        );
323
                    }
324
325
                    if ($this->userExists($value)) {
326
                        throw new User\Exception\NotUnique('user does already exists');
327
                    }
328
329
                break;
330
                case 'password':
331
                    if (!preg_match($this->password_policy, $value)) {
332
                        throw new User\Exception\InvalidArgument(
333
                            'password does not follow password policy '.$this->password_policy,
334
                            User\Exception\InvalidArgument::INVALID_PASSWORD
335
                        );
336
                    }
337
338
                    $value = password_hash($value, $this->password_hash);
339
340
                break;
341
                case 'soft_quota':
342
                case 'hard_quota':
343
                    if (!is_numeric($value)) {
344
                        throw new User\Exception\InvalidArgument(
345
                            $attribute.' must be numeric',
346
                            User\Exception\InvalidArgument::INVALID_QUOTA
347
                        );
348
                    }
349
350
                break;
351
                case 'avatar':
352
                    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...
353
                        throw new User\Exception\InvalidArgument(
354
                            'avatar must be an instance of Binary',
355
                            User\Exception\InvalidArgument::INVALID_AVATAR
356
                        );
357
                    }
358
359
                break;
360
                case 'mail':
361
                    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
362
                        throw new User\Exception\InvalidArgument(
363
                            'mail address given is invalid',
364
                            User\Exception\InvalidArgument::INVALID_MAIL
365
                        );
366
                    }
367
368
                break;
369
                case 'admin':
370
                    $value = (bool) $value;
371
372
                break;
373
                case 'namespace':
374
                    if (!is_string($value)) {
375
                        throw new User\Exception\InvalidArgument(
376
                            'namespace must be a valid string',
377
                            User\Exception\InvalidArgument::INVALID_NAMESPACE
378
                        );
379
                    }
380
381
                break;
382
                case 'optional':
383
                    if (!is_array($value)) {
384
                        throw new User\Exception\InvalidArgument(
385
                            'optional user attributes must be an array',
386
                            User\Exception\InvalidArgument::INVALID_OPTIONAL
387
                        );
388
                    }
389
390
                break;
391
                default:
392
                    throw new User\Exception\InvalidArgument(
393
                        'invalid attribute '.$attribute.' given',
394
                        User\Exception\InvalidArgument::INVALID_ATTRIBUTE
395
                    );
396
            }
397
        }
398
399
        return $attributes;
400
    }
401
402
    /**
403
     * Add user.
404
     *
405
     * @param string $username
406
     * @param array  $attributes
407
     *
408
     * @return ObjectId
409
     */
410
    public function addUser(string $username, array $attributes = []): ObjectId
411
    {
412
        $attributes['username'] = $username;
413
        $attributes = $this->validateUserAttributes($attributes);
414
415
        $defaults = [
416
            'created' => new UTCDateTime(),
417
            'changed' => new UTCDateTime(),
418
            'deleted' => false,
419
        ];
420
421
        $attributes = array_merge($defaults, $attributes);
422
        $result = $this->db->user->insertOne($attributes);
423
424
        return $result->getInsertedId();
425
    }
426
427
    /**
428
     * Check if user exists.
429
     *
430
     * @return bool
431
     */
432
    public function userExists(string $username): bool
433
    {
434
        return  1 === $this->db->user->count(['username' => $username]);
435
    }
436
437
    /**
438
     * Check if group exists.
439
     *
440
     * @return bool
441
     */
442
    public function groupExists(string $name): bool
443
    {
444
        return  1 === $this->db->group->count(['name' => $name]);
445
    }
446
447
    /**
448
     * Get user by id.
449
     *
450
     * @param ObjectId $id
451
     *
452
     * @return User
453
     */
454
    public function getUserById(ObjectId $id): User
455
    {
456
        $aggregation = $this->getUserAggregationPipes();
457
        array_unshift($aggregation, ['$match' => ['_id' => $id]]);
458
        $users = $this->db->user->aggregate($aggregation)->toArray();
459
460
        if (count($users) > 1) {
461
            throw new User\Exception\NotUnique('multiple user found');
462
        }
463
464
        if (count($users) === 0) {
465
            throw new User\Exception\NotFound('user does not exists');
466
        }
467
468
        return new User(array_shift($users), $this, $this->db, $this->logger);
469
    }
470
471
    /**
472
     * Get users by id.
473
     *
474
     * @param array $id
475
     *
476
     * @return Generator
477
     */
478
    public function getUsersById(array $id): Generator
479
    {
480
        $find = [];
481
        foreach ($id as $i) {
482
            $find[] = new ObjectId($i);
483
        }
484
485
        $filter = [
486
            '$match' => [
487
                '_id' => ['$in' => $find],
488
            ],
489
        ];
490
491
        $aggregation = $this->getUserAggregationPipes();
492
        array_unshift($aggregation, $filter);
493
        $users = $this->db->user->aggregate($aggregation);
494
495
        foreach ($users as $attributes) {
496
            yield new User($attributes, $this, $this->db, $this->logger);
497
        }
498
    }
499
500
    /**
501
     * Set Identity.
502
     *
503
     * @param Identity $identity
504
     *
505
     * @return bool
506
     */
507
    public function setIdentity(Identity $identity): bool
508
    {
509
        $user = null;
510
511
        try {
512
            $user = $this->getUserByName($identity->getIdentifier());
513
        } catch (User\Exception\NotFound $e) {
514
            $this->logger->warning('failed connect authenticated user, user account does not exists', [
515
                'category' => get_class($this),
516
            ]);
517
        }
518
519
        $this->hook->run('preServerIdentity', [$identity, &$user]);
520
521
        if (!($user instanceof User)) {
522
            throw new User\Exception\NotAuthenticated('user does not exists', User\Exception\NotAuthenticated::USER_NOT_FOUND);
523
        }
524
525
        if ($user->isDeleted()) {
526
            throw new User\Exception\NotAuthenticated(
527
                'user is disabled and can not be used',
528
                User\Exception\NotAuthenticated::USER_DELETED
529
            );
530
        }
531
532
        $this->identity = $user;
533
        $user->updateIdentity($identity)
534
             ->updateShares();
535
        $this->hook->run('postServerIdentity', [$user]);
536
537
        return true;
538
    }
539
540
    /**
541
     * Get authenticated user.
542
     *
543
     * @return User
544
     */
545
    public function getIdentity(): ?User
546
    {
547
        return $this->identity;
548
    }
549
550
    /**
551
     * Get user by name.
552
     *
553
     * @param string $name
554
     *
555
     * @return User
556
     */
557
    public function getUserByName(string $name): User
558
    {
559
        $aggregation = $this->getUserAggregationPipes();
560
        array_unshift($aggregation, ['$match' => ['username' => $name]]);
561
        $users = $this->db->user->aggregate($aggregation)->toArray();
562
563
        if (count($users) > 1) {
564
            throw new User\Exception\NotUnique('multiple user found');
565
        }
566
567
        if (count($users) === 0) {
568
            throw new User\Exception\NotFound('user does not exists');
569
        }
570
571
        return new User(array_shift($users), $this, $this->db, $this->logger);
572
    }
573
574
    /**
575
     * Count users.
576
     *
577
     * @param array $filter
578
     *
579
     * @return int
580
     */
581
    public function countUsers(array $filter): int
582
    {
583
        return $this->db->user->count($filter);
584
    }
585
586
    /**
587
     * Count groups.
588
     *
589
     * @param array $filter
590
     *
591
     * @return int
592
     */
593
    public function countGroups(array $filter): int
594
    {
595
        return $this->db->group->count($filter);
596
    }
597
598
    /**
599
     * Get users.
600
     *
601
     * @param array $filter
602
     * @param int   $offset
603
     * @param int   $limit
604
     *
605
     * @return Generator
606
     */
607
    public function getUsers(array $filter, ?int $offset = null, ?int $limit = null): Generator
608
    {
609
        $aggregation = $this->getUserAggregationPipes();
610
611
        if (count($filter) > 0) {
612
            array_unshift($aggregation, ['$match' => $filter]);
613
        }
614
615
        if ($offset !== null) {
616
            array_unshift($aggregation, ['$skip' => $offset]);
617
        }
618
619
        if ($limit !== null) {
620
            $aggregation[] = ['$limit' => $limit];
621
        }
622
623
        $users = $this->db->user->aggregate($aggregation);
624
625
        foreach ($users as $attributes) {
626
            yield new User($attributes, $this, $this->db, $this->logger);
627
        }
628
629
        return $this->db->user->count($filter);
630
    }
631
632
    /**
633
     * Get groups.
634
     *
635
     * @param array $filter
636
     * @param int   $offset
637
     * @param int   $limit
638
     *
639
     * @return Generator
640
     */
641
    public function getGroups(array $filter, ?int $offset = null, ?int $limit = null): Generator
642
    {
643
        $groups = $this->db->group->find($filter, [
644
            'skip' => $offset,
645
            'limit' => $limit,
646
        ]);
647
648
        foreach ($groups as $attributes) {
649
            yield new Group($attributes, $this, $this->db, $this->logger);
650
        }
651
652
        return $this->db->group->count($filter);
653
    }
654
655
    /**
656
     * Get group by name.
657
     *
658
     * @param string $name
659
     *
660
     * @return Group
661
     */
662
    public function getGroupByName(string $name): Group
663
    {
664
        $group = $this->db->group->findOne([
665
           'name' => $name,
666
        ]);
667
668
        if (null === $group) {
669
            throw new Group\Exception\NotFound('group does not exists');
670
        }
671
672
        return new Group($group, $this, $this->db, $this->logger);
673
    }
674
675
    /**
676
     * Get group by id.
677
     *
678
     * @param string $id
679
     *
680
     * @return Group
681
     */
682
    public function getGroupById(ObjectId $id): Group
683
    {
684
        $group = $this->db->group->findOne([
685
           '_id' => $id,
686
        ]);
687
688
        if (null === $group) {
689
            throw new Group\Exception\NotFound('group does not exists');
690
        }
691
692
        return new Group($group, $this, $this->db, $this->logger);
693
    }
694
695
    /**
696
     * Add group.
697
     *
698
     * @param string $name
699
     * @param array  $member
700
     * @param array  $attributes
701
     *
702
     * @return ObjectId
703
     */
704
    public function addGroup(string $name, array $member = [], array $attributes = []): ObjectId
705
    {
706
        $attributes['member'] = $member;
707
        $attributes['name'] = $name;
708
        $attributes = $this->validateGroupAttributes($attributes);
709
710
        $defaults = [
711
            'created' => new UTCDateTime(),
712
            'changed' => new UTCDateTime(),
713
            'deleted' => false,
714
        ];
715
716
        $attributes = array_merge($attributes, $defaults);
717
        $result = $this->db->group->insertOne($attributes);
718
719
        return $result->getInsertedId();
720
    }
721
722
    /**
723
     * Get user aggregation pipe.
724
     *
725
     * @return array
726
     */
727
    protected function getUserAggregationPipes(): array
728
    {
729
        return [
730
            ['$lookup' => [
731
                'from' => 'group',
732
                'localField' => '_id',
733
                'foreignField' => 'member',
734
                'as' => 'groups',
735
            ]],
736
            ['$addFields' => [
737
                'groups' => [
738
                    '$map' => [
739
                        'input' => '$groups',
740
                        'as' => 'group',
741
                        'in' => '$$group._id',
742
                    ],
743
                ],
744
            ]],
745
        ];
746
    }
747
}
748