Completed
Push — master ( 264cb1...309a3e )
by Raffael
06:50
created

Server::groupNameExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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