Passed
Push — master ( a9f547...49cca7 )
by Raffael
04:18
created

Server::getIdentity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

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