Completed
Pull Request — master (#149)
by Raffael
05:07
created

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