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
|
|||
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 |
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 thecomposer.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
orrequire-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 you have not tested against this specific condition, such errors might go unnoticed.