Completed
Push — master ( 287393...7ff50d )
by Raffael
18:27 queued 14:12
created

src/lib/Server.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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