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\App\Api\v2; |
||
13 | |||
14 | use Balloon\App\Api\Helper as ApiHelper; |
||
15 | use Balloon\App\Api\v2\Collections as ApiCollection; |
||
16 | use Balloon\App\Api\v2\Files as ApiFile; |
||
17 | use Balloon\AttributeDecorator\Pager; |
||
18 | use Balloon\Filesystem; |
||
19 | use Balloon\Filesystem\DeltaAttributeDecorator; |
||
20 | use Balloon\Filesystem\EventAttributeDecorator; |
||
21 | use Balloon\Filesystem\Exception; |
||
22 | use Balloon\Filesystem\Node\AttributeDecorator as NodeAttributeDecorator; |
||
23 | use Balloon\Filesystem\Node\Collection; |
||
24 | use Balloon\Filesystem\Node\NodeInterface; |
||
25 | use Balloon\Helper; |
||
26 | use Balloon\Server; |
||
27 | use Balloon\Server\User; |
||
28 | use Micro\Http\Response; |
||
29 | use function MongoDB\BSON\fromJSON; |
||
30 | use function MongoDB\BSON\toPHP; |
||
31 | use MongoDB\BSON\UTCDateTime; |
||
32 | use Psr\Log\LoggerInterface; |
||
33 | use ZipStream\ZipStream; |
||
34 | |||
35 | class Nodes extends Controller |
||
36 | { |
||
37 | /** |
||
38 | * Filesystem. |
||
39 | * |
||
40 | * @var Filesystem |
||
41 | */ |
||
42 | protected $fs; |
||
43 | |||
44 | /** |
||
45 | * LoggerInterface. |
||
46 | * |
||
47 | * @var LoggerInterface |
||
48 | */ |
||
49 | protected $logger; |
||
50 | |||
51 | /** |
||
52 | * Server. |
||
53 | * |
||
54 | * @var Server |
||
55 | */ |
||
56 | protected $server; |
||
57 | |||
58 | /** |
||
59 | * User. |
||
60 | * |
||
61 | * @var User |
||
62 | */ |
||
63 | protected $user; |
||
64 | |||
65 | /** |
||
66 | * Node attribute decorator. |
||
67 | * |
||
68 | * @var NodeAttributeDecorator |
||
69 | */ |
||
70 | protected $node_decorator; |
||
71 | |||
72 | /** |
||
73 | * Initialize. |
||
74 | */ |
||
75 | public function __construct(Server $server, NodeAttributeDecorator $decorator, LoggerInterface $logger) |
||
76 | { |
||
77 | $this->fs = $server->getFilesystem(); |
||
78 | $this->user = $server->getIdentity(); |
||
79 | $this->server = $server; |
||
80 | $this->node_decorator = $decorator; |
||
81 | $this->logger = $logger; |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * Restore node. |
||
86 | */ |
||
87 | public function postUndelete( |
||
88 | $id, |
||
89 | bool $move = false, |
||
90 | ?string $destid = null, |
||
91 | int $conflict = 0 |
||
92 | ): Response { |
||
93 | $parent = null; |
||
94 | if (true === $move) { |
||
95 | try { |
||
96 | $parent = $this->fs->getNode($destid, Collection::class, false, true); |
||
97 | } catch (Exception\NotFound $e) { |
||
98 | throw new Exception\NotFound( |
||
99 | 'destination collection was not found or is not a collection', |
||
100 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
101 | ); |
||
102 | } |
||
103 | } |
||
104 | |||
105 | return $this->bulk($id, function ($node) use ($parent, $conflict, $move) { |
||
106 | if (true === $move) { |
||
107 | $node = $node->setParent($parent, $conflict); |
||
108 | } |
||
109 | |||
110 | $node->undelete($conflict); |
||
111 | |||
112 | return [ |
||
113 | 'code' => 200, |
||
114 | 'data' => $this->node_decorator->decorate($node), |
||
115 | ]; |
||
116 | }); |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Download stream. |
||
121 | * |
||
122 | * @param null|mixed $id |
||
123 | */ |
||
124 | public function getContent( |
||
125 | $id = null, |
||
126 | bool $download = false, |
||
127 | string $name = 'selected', |
||
128 | ?string $encoding = null |
||
0 ignored issues
–
show
|
|||
129 | ): ?Response { |
||
130 | if (is_array($id)) { |
||
131 | return $this->combine($id, $name); |
||
132 | } |
||
133 | |||
134 | $node = $this->_getNode($id); |
||
135 | if ($node instanceof Collection) { |
||
136 | return $node->getZip(); |
||
137 | } |
||
138 | |||
139 | $response = new Response(); |
||
140 | |||
141 | return ApiHelper::streamContent($response, $node, $download); |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Get attributes. |
||
146 | * |
||
147 | * @param null|mixed $id |
||
148 | * @param null|mixed $query |
||
149 | */ |
||
150 | public function get($id = null, int $deleted = 0, $query = null, array $attributes = [], int $offset = 0, int $limit = 20): Response |
||
151 | { |
||
152 | if ($id === null) { |
||
153 | $query = $this->parseQuery($query); |
||
154 | |||
155 | if ($this instanceof ApiFile) { |
||
156 | $query['directory'] = false; |
||
157 | $uri = '/api/v2/files'; |
||
158 | } elseif ($this instanceof ApiCollection) { |
||
159 | $query['directory'] = true; |
||
160 | $uri = '/api/v2/collections'; |
||
161 | } else { |
||
162 | $uri = '/api/v2/nodes'; |
||
163 | } |
||
164 | |||
165 | $nodes = $this->fs->findNodesByFilterUser($deleted, $query, $offset, $limit); |
||
166 | $pager = new Pager($this->node_decorator, $nodes, $attributes, $offset, $limit, $uri); |
||
167 | $result = $pager->paging(); |
||
168 | |||
169 | return (new Response())->setCode(200)->setBody($result); |
||
170 | } |
||
171 | |||
172 | return $this->bulk($id, function ($node) use ($attributes) { |
||
173 | return [ |
||
174 | 'code' => 200, |
||
175 | 'data' => $this->node_decorator->decorate($node, $attributes), |
||
176 | ]; |
||
177 | }); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Get parent nodes. |
||
182 | */ |
||
183 | public function getParents(string $id, array $attributes = [], bool $self = false): Response |
||
184 | { |
||
185 | $result = []; |
||
186 | $request = $this->_getNode($id); |
||
187 | $parents = $request->getParents(); |
||
188 | |||
189 | if (true === $self && $request instanceof Collection) { |
||
190 | $result[] = $this->node_decorator->decorate($request, $attributes); |
||
191 | } |
||
192 | |||
193 | foreach ($parents as $node) { |
||
194 | $result[] = $this->node_decorator->decorate($node, $attributes); |
||
195 | } |
||
196 | |||
197 | return (new Response())->setCode(200)->setBody($result); |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Change attributes. |
||
202 | * |
||
203 | * @param null|mixed $lock |
||
204 | */ |
||
205 | public function patch(string $id, ?string $name = null, ?array $meta = null, ?bool $readonly = null, ?array $filter = null, ?array $acl = null, $lock = null): Response |
||
206 | { |
||
207 | $attributes = compact('name', 'meta', 'readonly', 'filter', 'acl', 'lock'); |
||
208 | $attributes = array_filter($attributes, function ($attribute) {return !is_null($attribute); }); |
||
209 | |||
210 | $lock = $_SERVER['HTTP_LOCK_TOKEN'] ?? null; |
||
211 | |||
212 | return $this->bulk($id, function ($node) use ($attributes, $lock) { |
||
213 | foreach ($attributes as $attribute => $value) { |
||
214 | switch ($attribute) { |
||
215 | case 'name': |
||
216 | $node->setName($value); |
||
217 | |||
218 | break; |
||
219 | case 'meta': |
||
220 | $node->setMetaAttributes($value); |
||
221 | |||
222 | break; |
||
223 | case 'readonly': |
||
224 | $node->setReadonly($value); |
||
225 | |||
226 | break; |
||
227 | case 'filter': |
||
228 | if ($node instanceof Collection) { |
||
229 | $node->setFilter($value); |
||
230 | } |
||
231 | |||
232 | break; |
||
233 | case 'acl': |
||
234 | $node->setAcl($value); |
||
235 | |||
236 | break; |
||
237 | case 'lock': |
||
238 | if ($value === false) { |
||
239 | $node->unlock($lock); |
||
240 | } else { |
||
241 | $node->lock($lock); |
||
242 | } |
||
243 | |||
244 | break; |
||
245 | } |
||
246 | } |
||
247 | |||
248 | return [ |
||
249 | 'code' => 200, |
||
250 | 'data' => $this->node_decorator->decorate($node), |
||
251 | ]; |
||
252 | }); |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * Clone node. |
||
257 | */ |
||
258 | public function postClone( |
||
259 | $id, |
||
260 | ?string $destid = null, |
||
261 | int $conflict = 0 |
||
262 | ): Response { |
||
263 | try { |
||
264 | $parent = $this->fs->getNode($destid, Collection::class, false, true); |
||
265 | } catch (Exception\NotFound $e) { |
||
266 | throw new Exception\NotFound( |
||
267 | 'destination collection was not found or is not a collection', |
||
268 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
269 | ); |
||
270 | } |
||
271 | |||
272 | return $this->bulk($id, function ($node) use ($parent, $conflict) { |
||
273 | $result = $node->copyTo($parent, $conflict); |
||
274 | |||
275 | return [ |
||
276 | 'code' => $parent == $result ? 200 : 201, |
||
277 | 'data' => $this->node_decorator->decorate($result), |
||
278 | ]; |
||
279 | }); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Move node. |
||
284 | */ |
||
285 | public function postMove( |
||
286 | $id, |
||
287 | ?string $destid = null, |
||
288 | int $conflict = 0 |
||
289 | ): Response { |
||
290 | try { |
||
291 | $parent = $this->fs->getNode($destid, Collection::class, false, true); |
||
292 | } catch (Exception\NotFound $e) { |
||
293 | throw new Exception\NotFound( |
||
294 | 'destination collection was not found or is not a collection', |
||
295 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
296 | ); |
||
297 | } |
||
298 | |||
299 | return $this->bulk($id, function ($node) use ($parent, $conflict) { |
||
300 | $result = $node->setParent($parent, $conflict); |
||
301 | |||
302 | return [ |
||
303 | 'code' => 200, |
||
304 | 'data' => $this->node_decorator->decorate($node), |
||
305 | ]; |
||
306 | }); |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * Delete node. |
||
311 | */ |
||
312 | public function delete( |
||
313 | $id, |
||
314 | bool $force = false, |
||
315 | bool $ignore_flag = false, |
||
316 | ?string $at = null |
||
317 | ): Response { |
||
318 | $failures = []; |
||
319 | |||
320 | if (null !== $at && '0' !== $at) { |
||
321 | $at = $this->_verifyAttributes(['destroy' => $at])['destroy']; |
||
322 | } |
||
323 | |||
324 | return $this->bulk($id, function ($node) use ($force, $ignore_flag, $at) { |
||
325 | if (null === $at) { |
||
326 | $node->delete($force && $node->isDeleted() || $force && $ignore_flag); |
||
327 | } else { |
||
328 | if ('0' === $at) { |
||
329 | $at = null; |
||
330 | } |
||
331 | $node->setDestroyable($at); |
||
332 | } |
||
333 | |||
334 | return [ |
||
335 | 'code' => 204, |
||
336 | ]; |
||
337 | }); |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * Get trash. |
||
342 | * |
||
343 | * @param null|mixed $query |
||
344 | */ |
||
345 | public function getTrash($query = null, array $attributes = [], int $offset = 0, int $limit = 20): Response |
||
346 | { |
||
347 | $children = []; |
||
348 | $query = $this->parseQuery($query); |
||
349 | $filter = ['deleted' => ['$type' => 9]]; |
||
350 | |||
351 | if (!empty($query)) { |
||
352 | $filter = [ |
||
353 | $filter, |
||
354 | $query, |
||
355 | ]; |
||
356 | } |
||
357 | |||
358 | $nodes = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_ONLY, $filter, $offset, $limit); |
||
359 | |||
360 | foreach ($nodes as $node) { |
||
361 | try { |
||
362 | $parent = $node->getParent(); |
||
363 | if (null !== $parent && $parent->isDeleted()) { |
||
364 | continue; |
||
365 | } |
||
366 | } catch (\Exception $e) { |
||
367 | //skip exception |
||
368 | } |
||
369 | |||
370 | $children[] = $node; |
||
371 | } |
||
372 | |||
373 | if ($this instanceof ApiFile) { |
||
374 | $query['directory'] = false; |
||
375 | $uri = '/api/v2/files'; |
||
376 | } elseif ($this instanceof ApiCollection) { |
||
377 | $query['directory'] = true; |
||
378 | $uri = '/api/v2/collections'; |
||
379 | } else { |
||
380 | $uri = '/api/v2/nodes'; |
||
381 | } |
||
382 | |||
383 | $pager = new Pager($this->node_decorator, $children, $attributes, $offset, $limit, $uri, $nodes->getReturn()); |
||
384 | $result = $pager->paging(); |
||
385 | |||
386 | return (new Response())->setCode(200)->setBody($result); |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * Get delta. |
||
391 | */ |
||
392 | public function getDelta( |
||
393 | DeltaAttributeDecorator $delta_decorator, |
||
394 | ?string $id = null, |
||
395 | ?string $cursor = null, |
||
396 | int $limit = 250, |
||
397 | array $attributes = [] |
||
398 | ): Response { |
||
399 | if (null !== $id) { |
||
400 | $node = $this->_getNode($id); |
||
401 | } else { |
||
402 | $node = null; |
||
403 | } |
||
404 | |||
405 | $result = $this->fs->getDelta()->getDeltaFeed($cursor, $limit, $node); |
||
406 | foreach ($result['nodes'] as &$node) { |
||
407 | if ($node instanceof NodeInterface) { |
||
408 | $node = $this->node_decorator->decorate($node, $attributes); |
||
409 | } else { |
||
410 | $node = $delta_decorator->decorate($node, $attributes); |
||
411 | } |
||
412 | } |
||
413 | |||
414 | return (new Response())->setCode(200)->setBody($result); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Event log. |
||
419 | * |
||
420 | * @param null|mixed $query |
||
421 | */ |
||
422 | public function getEventLog(EventAttributeDecorator $event_decorator, ?string $id = null, $query = null, ?string $sort = null, ?array $attributes = [], int $offset = 0, int $limit = 20): Response |
||
0 ignored issues
–
show
|
|||
423 | { |
||
424 | if (null !== $id) { |
||
425 | $node = $this->_getNode($id); |
||
426 | $uri = '/api/v2/nodes/'.$node->getId().'/event-log'; |
||
427 | } else { |
||
428 | $node = null; |
||
429 | $uri = '/api/v2/nodes/event-log'; |
||
430 | } |
||
431 | |||
432 | $query = $this->parseQuery($query); |
||
433 | $result = $this->fs->getDelta()->getEventLog($query, $limit, $offset, $node, $total); |
||
434 | $pager = new Pager($event_decorator, $result, $attributes, $offset, $limit, $uri, $total); |
||
435 | |||
436 | return (new Response())->setCode(200)->setBody($pager->paging()); |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * Get last Cursor. |
||
441 | */ |
||
442 | public function getLastCursor(?string $id = null): Response |
||
443 | { |
||
444 | if (null !== $id) { |
||
445 | $node = $this->_getNode($id); |
||
446 | } else { |
||
447 | $node = null; |
||
448 | } |
||
449 | |||
450 | $result = $this->fs->getDelta()->getLastCursor(); |
||
451 | |||
452 | return (new Response())->setCode(200)->setBody($result); |
||
453 | } |
||
454 | |||
455 | /** |
||
456 | * Parse query. |
||
457 | */ |
||
458 | protected function parseQuery($query): array |
||
459 | { |
||
460 | if ($query === null) { |
||
461 | $query = []; |
||
462 | } elseif (is_string($query)) { |
||
463 | $query = toPHP(fromJSON($query), [ |
||
464 | 'root' => 'array', |
||
465 | 'document' => 'array', |
||
466 | 'array' => 'array', |
||
467 | ]); |
||
468 | } |
||
469 | |||
470 | return (array) $query; |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * Merge multiple nodes into one zip archive. |
||
475 | * |
||
476 | * @param null|mixed $id |
||
477 | */ |
||
478 | protected function combine($id = null, string $name = 'selected') |
||
479 | { |
||
480 | $archive = new ZipStream($name.'.zip'); |
||
481 | |||
482 | foreach ($this->_getNodes($id) as $node) { |
||
483 | try { |
||
484 | $node->zip($archive); |
||
485 | } catch (\Exception $e) { |
||
486 | $this->logger->debug('failed zip node in multi node request ['.$node->getId().']', [ |
||
487 | 'category' => get_class($this), |
||
488 | 'exception' => $e, |
||
489 | ]); |
||
490 | } |
||
491 | } |
||
492 | |||
493 | $archive->finish(); |
||
494 | } |
||
495 | |||
496 | /** |
||
497 | * Check custom node attributes which have to be written. |
||
498 | */ |
||
499 | protected function _verifyAttributes(array $attributes): array |
||
500 | { |
||
501 | $valid_attributes = [ |
||
502 | 'changed', |
||
503 | 'destroy', |
||
504 | 'created', |
||
505 | 'meta', |
||
506 | 'readonly', |
||
507 | 'acl', |
||
508 | 'lock', |
||
509 | ]; |
||
510 | |||
511 | if ($this instanceof ApiCollection) { |
||
512 | $valid_attributes += ['filter', 'mount']; |
||
513 | } |
||
514 | |||
515 | $check = array_merge(array_flip($valid_attributes), $attributes); |
||
516 | |||
517 | if ($this instanceof ApiCollection && count($check) > 9) { |
||
518 | throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, lock, filter, mount, readonly and/or meta attributes may be overwritten'); |
||
519 | } |
||
520 | if ($this instanceof ApiFile && count($check) > 7) { |
||
521 | throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, lock, readonly and/or meta attributes may be overwritten'); |
||
522 | } |
||
523 | |||
524 | foreach ($attributes as $attribute => $value) { |
||
525 | switch ($attribute) { |
||
526 | case 'filter': |
||
527 | if (!is_array($value)) { |
||
528 | throw new Exception\InvalidArgument($attribute.' must be an array'); |
||
529 | } |
||
530 | |||
531 | $attributes['filter'] = json_encode($value); |
||
532 | |||
533 | break; |
||
534 | case 'destroy': |
||
535 | if (!Helper::isValidTimestamp($value)) { |
||
536 | throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp'); |
||
537 | } |
||
538 | $attributes[$attribute] = new UTCDateTime($value.'000'); |
||
539 | |||
540 | break; |
||
541 | case 'changed': |
||
542 | case 'created': |
||
543 | if (!Helper::isValidTimestamp($value)) { |
||
544 | throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp'); |
||
545 | } |
||
546 | if ((int) $value > time()) { |
||
547 | throw new Exception\InvalidArgument($attribute.' timestamp can not be set greater than the server time'); |
||
548 | } |
||
549 | $attributes[$attribute] = new UTCDateTime($value.'000'); |
||
550 | |||
551 | break; |
||
552 | case 'readonly': |
||
553 | $attributes['readonly'] = (bool) $attributes['readonly']; |
||
554 | |||
555 | break; |
||
556 | } |
||
557 | } |
||
558 | |||
559 | return $attributes; |
||
560 | } |
||
561 | } |
||
562 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.