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

src/app/Balloon.App.Api/v2/Nodes.php (2 issues)

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\App\Api\v2;
13
14
use Balloon\App\Api\Controller;
15
use Balloon\App\Api\Helper as ApiHelper;
16
use Balloon\App\Api\v2\Collections as ApiCollection;
17
use Balloon\App\Api\v2\Files as ApiFile;
18
use Balloon\AttributeDecorator\Pager;
19
use Balloon\Filesystem;
20
use Balloon\Filesystem\DeltaAttributeDecorator;
21
use Balloon\Filesystem\EventAttributeDecorator;
22
use Balloon\Filesystem\Exception;
23
use Balloon\Filesystem\Node\AttributeDecorator as NodeAttributeDecorator;
24
use Balloon\Filesystem\Node\Collection;
25
use Balloon\Filesystem\Node\File;
26
use Balloon\Filesystem\Node\NodeInterface;
27
use Balloon\Helper;
28
use Balloon\Server;
29
use Balloon\Server\User;
30
use Micro\Http\Response;
31
use function MongoDB\BSON\fromJSON;
32
use function MongoDB\BSON\toPHP;
33
use MongoDB\BSON\UTCDateTime;
34
use Psr\Log\LoggerInterface;
35
use ZipStream\ZipStream;
36
37
class Nodes extends Controller
38
{
39
    /**
40
     * Filesystem.
41
     *
42
     * @var Filesystem
43
     */
44
    protected $fs;
45
46
    /**
47
     * LoggerInterface.
48
     *
49
     * @var LoggerInterface
50
     */
51
    protected $logger;
52
53
    /**
54
     * Server.
55
     *
56
     * @var Server
57
     */
58
    protected $server;
59
60
    /**
61
     * User.
62
     *
63
     * @var User
64
     */
65
    protected $user;
66
67
    /**
68
     * Node attribute decorator.
69
     *
70
     * @var NodeAttributeDecorator
71
     */
72
    protected $node_decorator;
73
74
    /**
75
     * Initialize.
76
     */
77
    public function __construct(Server $server, NodeAttributeDecorator $decorator, LoggerInterface $logger)
78
    {
79
        $this->fs = $server->getFilesystem();
80
        $this->user = $server->getIdentity();
81
        $this->server = $server;
82
        $this->node_decorator = $decorator;
83
        $this->logger = $logger;
84
    }
85
86
    /**
87
     * @api {head} /api/v2/nodes/:id Node exists?
88
     * @apiVersion 2.0.0
89
     * @apiName head
90
     * @apiGroup Node
91
     * @apiPermission none
92
     * @apiDescription Check if a node exists. Per default deleted nodes are ignore which means it will
93
     *  return a 404 if a deleted node is requested. You can change this behaviour via the deleted parameter.
94
     * @apiUse _getNode
95
     *
96
     * @apiExample (cURL) example:
97
     * curl -XHEAD "https://SERVER/api/v2/node?id=544627ed3c58891f058b4686"
98
     * curl -XHEAD "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686"
99
     * curl -XHEAD "https://SERVER/api/v2/node?p=/absolute/path/to/my/node"
100
     *
101
     * @apiParam (GET Parameter) {number} [deleted=0] Wherever include deleted node or not, possible values:</br>
102
     * - 0 Exclude deleted</br>
103
     * - 1 Only deleted</br>
104
     * - 2 Include deleted</br>
105
     *
106
     * @apiSuccessExample {json} Success-Response (Node does exist):
107
     * HTTP/1.1 200 OK
108
     *
109
     * @apiSuccessExample {json} Success-Response (Node does not exist):
110
     * HTTP/1.1 404 Not Found
111
     *
112
     * @param string $id
113
     * @param string $p
114
     */
115
    public function head(?string $id = null, ?string $p = null, int $deleted = 0): Response
116
    {
117
        try {
118
            $result = $this->_getNode($id, $p, null, false, false, $deleted);
119
120
            $response = (new Response())
121
                ->setHeader('Content-Length', (string) $result->getSize())
122
                ->setHeader('Content-Type', $result->getContentType())
123
                ->setCode(200);
124
125
            return $response;
126
        } catch (\Exception $e) {
127
            return (new Response())->setCode(404);
128
        }
129
    }
130
131
    /**
132
     * @api {post} /api/v2/nodes/:id/undelete Restore node
133
     * @apiVersion 2.0.0
134
     * @apiName postUndelete
135
     * @apiGroup Node
136
     * @apiPermission none
137
     * @apiDescription Undelete (Restore from trash) a single node or multiple ones.
138
     * @apiUse _getNodes
139
     * @apiUse _conflictNode
140
     * @apiUse _multiError
141
     * @apiUse _writeAction
142
     *
143
     * @apiExample (cURL) example:
144
     * curl -XPOST "https://SERVER/api/v2/nodes/undelete?id[]=544627ed3c58891f058b4686&id[]=544627ed3c58891f058b46865&pretty"
145
     * curl -XPOST "https://SERVER/api/v2/nodes/undelete?id=544627ed3c58891f058b4686?pretty"
146
     * curl -XPOST "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686/undelete?conflict=2"
147
     * curl -XPOST "https://SERVER/api/v2/nodes/undelete?p=/absolute/path/to/my/node&conflict=0&move=1&destid=544627ed3c58891f058b46889"
148
     *
149
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
150
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
151
     *
152
     * @apiSuccessExample {json} Success-Response (conflict=1):
153
     * HTTP/1.1 200 OK
154
     * {
155
     *      "id": "544627ed3c58891f058b4686",
156
     *      "name": "renamed (xy23)"
157
     * }
158
     *
159
     * @param array|string $id
160
     * @param array|string $p
161
     * @param string       $destid
162
     * @param string       $destp
163
     */
164
    public function postUndelete(
165
        $id = null,
166
        $p = null,
167
        bool $move = false,
168
        ?string $destid = null,
169
        ?string $destp = null,
170
        int $conflict = 0
171
    ): Response {
172
        $parent = null;
173
        if (true === $move) {
174
            try {
175
                $parent = $this->_getNode($destid, $destp, 'Collection', false, true);
176
            } catch (Exception\NotFound $e) {
177
                throw new Exception\NotFound(
178
                    'destination collection was not found or is not a collection',
179
                    Exception\NotFound::DESTINATION_NOT_FOUND
180
                );
181
            }
182
        }
183
184
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict, $move) {
185
            if (true === $move) {
186
                $node = $node->setParent($parent, $conflict);
187
            }
188
189
            $node->undelete($conflict);
190
191
            return [
192
                'code' => 200,
193
                'data' => $this->node_decorator->decorate($node),
194
            ];
195
        });
196
    }
197
198
    /**
199
     * @api {get} /api/v2/nodes/:id/content Download stream
200
     * @apiVersion 2.0.0
201
     * @apiName getContent
202
     * @apiGroup Node
203
     * @apiPermission none
204
     * @apiDescription Download node contents. Collections are zipped during streaming.
205
     * @apiUse _getNode
206
     * @apiParam (GET Parameter) {boolean} [download=false] Force download file (Content-Disposition: attachment HTTP header)
207
     *
208
     * @apiExample (cURL) example:
209
     * curl -XGET "https://SERVER/api/v2/node?id=544627ed3c58891f058b4686" > myfile.txt
210
     * curl -XGET "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686" > myfile.txt
211
     * curl -XGET "https://SERVER/api/v2/node?p=/absolute/path/to/my/collection" > folder.zip
212
     *
213
     * @apiSuccessExample {binary} Success-Response:
214
     * HTTP/1.1 200 OK
215
     *
216
     * @apiErrorExample {json} Error-Response (Invalid offset):
217
     * HTTP/1.1 400 Bad Request
218
     * {
219
     *      "status": 400,
220
     *      "data": {
221
     *          "error": "Balloon\\Exception\\Conflict",
222
     *          "message": "invalid offset requested",
223
     *          "code": 277
224
     *      }
225
     * }
226
     *
227
     * @param array|string $id
228
     * @param array|string $p
229
     */
230
    public function getContent(
231
        $id = null,
232
        $p = null,
233
        bool $download = false,
234
        string $name = 'selected'
235
    ): ?Response {
236
        if (is_array($id) || is_array($p)) {
237
            return $this->combine($id, $p, $name);
238
        }
239
240
        $node = $this->_getNode($id, $p);
241
        if ($node instanceof Collection) {
242
            return $node->getZip();
243
        }
244
245
        $response = new Response();
246
247
        return ApiHelper::streamContent($response, $node, $download);
248
    }
249
250
    /**
251
     * @apiDefine _nodeAttributes
252
     *
253
     * @apiSuccess (200 OK) {string} id Unique node id
254
     * @apiSuccess (200 OK) {string} name Name
255
     * @apiSuccess (200 OK) {string} hash MD5 content checksum (file only)
256
     * @apiSuccess (200 OK) {object} meta Extended meta attributes
257
     * @apiSuccess (200 OK) {string} meta.description UTF-8 Text Description
258
     * @apiSuccess (200 OK) {string} meta.color Color Tag (HEX) (Like: #000000)
259
     * @apiSuccess (200 OK) {string} meta.author Author
260
     * @apiSuccess (200 OK) {string} meta.mail Mail contact address
261
     * @apiSuccess (200 OK) {string} meta.license License
262
     * @apiSuccess (200 OK) {string} meta.copyright Copyright string
263
     * @apiSuccess (200 OK) {string[]} meta.tags Search Tags
264
     * @apiSuccess (200 OK) {number} size Size in bytes (file only), number of children if collection
265
     * @apiSuccess (200 OK) {string} mime Mime type
266
     * @apiSuccess (200 OK) {boolean} sharelink Is node shared?
267
     * @apiSuccess (200 OK) {number} version File version (file only)
268
     * @apiSuccess (200 OK) {mixed} deleted Is boolean false if not deleted, if deleted it contains a deleted timestamp
269
     * @apiSuccess (200 OK) {string} deleted ISO8601 timestamp, only set if node is deleted
270
     * @apiSuccess (200 OK) {string} changed ISO8601 timestamp
271
     * @apiSuccess (200 OK) {string} created ISO8601 timestamp
272
     * @apiSuccess (200 OK) {string} destroy ISO8601 timestamp, only set if node has a destroy timestamp set
273
     * @apiSuccess (200 OK) {boolean} share Node is shared
274
     * @apiSuccess (200 OK) {boolean} directory Is true if the node is a collection
275
     * @apiSuccess (200 OK) {string} access Access permission for the authenticated user (d/r/rw/m)
276
     * @apiSuccess (200 OK) {object} shareowner Share owner
277
     * @apiSuccess (200 OK) {object} parent Parent node
278
     * @apiSuccess (200 OK) {string} path Absolute node path
279
     * @apiSuccess (200 OK) {string} filter Node is filtered (collection only)
280
     * @apiSuccess (200 OK) {boolean} readonly Readonly
281
     *
282
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes
283
     *
284
     * @param null|mixed $id
285
     * @param null|mixed $p
286
     * @param null|mixed $query
287
     */
288
289
    /**
290
     * @api {get} /api/v2/nodes/:id Get attributes
291
     * @apiVersion 2.0.0
292
     * @apiName get
293
     * @apiGroup Node
294
     * @apiPermission none
295
     * @apiDescription Get attributes from one or multiple nodes
296
     * @apiUse _getNode
297
     * @apiUse _nodeAttributes
298
     *
299
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default only a bunch of attributes would be returned, if you
300
     * need other attributes you have to request them (for example "path")
301
     *
302
     * @apiExample (cURL) example:
303
     * curl -XGET "https://SERVER/api/v2/node?id=544627ed3c58891f058b4686&pretty"
304
     * curl -XGET "https://SERVER/api/v2/node?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty"
305
     * curl -XGET "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686?pretty"
306
     * curl -XGET "https://SERVER/api/v2/node?p=/absolute/path/to/my/node&pretty"
307
     *
308
     * @apiSuccessExample {json} Success-Response:
309
     * HTTP/1.1 200 OK
310
     * {
311
     *      "id": "544627ed3c58891f058b4686",
312
     *      "name": "api.php",
313
     *      "hash": "a77f23ed800fd7a600a8c2cfe8cc370b",
314
     *      "meta": {
315
     *          "license": "GPLv3"
316
     *      },
317
     *      "size": 178,
318
     *      "mime": "text\/plain",
319
     *      "sharelink": true,
320
     *      "version": 1,
321
     *      "changed": "2007-08-31T16:47+00:00",
322
     *      "created": "2007-08-31T16:47+00:00",
323
     *      "share": false,
324
     *      "directory": false
325
     * }
326
     *
327
     * @param array|string $id
328
     * @param array|string $p
329
     */
330
    public function get($id = null, $p = null, int $deleted = 0, $query = null, array $attributes = [], int $offset = 0, int $limit = 20): Response
331
    {
332
        if ($id === null && $p === null) {
333
            if ($query === null) {
334
                $query = [];
335
            } elseif (is_string($query)) {
336
                $query = toPHP(fromJSON($query), [
337
                    'root' => 'array',
338
                    'document' => 'array',
339
                    'array' => 'array',
340
                ]);
341
            }
342
343
            if ($this instanceof ApiFile) {
344
                $query['directory'] = false;
345
                $uri = '/api/v2/files';
346
            } elseif ($this instanceof ApiCollection) {
347
                $query['directory'] = true;
348
                $uri = '/api/v2/collections';
349
            } else {
350
                $uri = '/api/v2/nodes';
351
            }
352
353
            $nodes = $this->fs->findNodesByFilterUser($deleted, $query, $offset, $limit);
354
            $pager = new Pager($this->node_decorator, $nodes, $attributes, $offset, $limit, $uri);
355
            $result = $pager->paging();
356
357
            return (new Response())->setCode(200)->setBody($result);
358
        }
359
360
        return $this->bulk($id, $p, function ($node) use ($attributes) {
361
            return [
362
                'code' => 200,
363
                'data' => $this->node_decorator->decorate($node, $attributes),
364
            ];
365
        });
366
    }
367
368
    /**
369
     * @api {get} /api/v2/nodes/:id/parents Get parent nodes
370
     * @apiVersion 2.0.0
371
     * @apiName getParents
372
     * @apiGroup Node
373
     * @apiPermission none
374
     * @apiDescription Get system attributes of all parent nodes. The hirarchy of all parent nodes is ordered in a
375
     * single level array beginning with the collection on the highest level.
376
     * @apiUse _getNode
377
     * @apiUse _nodeAttributes
378
     *
379
     * @apiParam (GET Parameter) {boolean} [self=true] Include requested collection itself at the end of the list (Will be ignored if the requested node is a file)
380
     *
381
     * @apiExample (cURL) example:
382
     * curl -XGET "https://SERVER/api/v2/nodes/parents?id=544627ed3c58891f058b4686&pretty"
383
     * curl -XGET "https://SERVER/api/v2/nodes/parents?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty"
384
     * curl -XGET "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686/parents?pretty&self=1"
385
     * curl -XGET "https://SERVER/api/v2/nodes/parents?p=/absolute/path/to/my/node&self=1"
386
     *
387
     * @apiSuccessExample {json} Success-Response:
388
     * HTTP/1.1 200 OK
389
     * [
390
     *  {
391
     *      "id": "544627ed3c58891f058bbbaa",
392
     *      "name": "rootdir",
393
     *      "meta": {},
394
     *      "size": 1,
395
     *      "mime": "inode/directory",
396
     *      "created": "2007-08-31T16:47+00:00",
397
     *      "changed": "2007-08-31T16:47+00:00",
398
     *      "destroy": "2020-08-31T16:47+00:00",
399
     *      "share": false,
400
     *      "directory": true
401
     *  },
402
     *  {
403
     *      "id": "544627ed3c58891f058b46cc",
404
     *      "name": "parentdir",
405
     *      "meta": {},
406
     *      "size": 3,
407
     *      "mime": "inode/directory",
408
     *      "created": "2007-08-31T16:47+00:00",
409
     *      "changed": "2007-08-31T16:47+00:00",
410
     *      "share": false,
411
     *      "directory": true
412
     *  }
413
     * ]
414
     *
415
     * @param string $id
416
     * @param string $p
417
     */
418
    public function getParents(?string $id = null, ?string $p = null, array $attributes = [], bool $self = false): Response
419
    {
420
        $result = [];
421
        $request = $this->_getNode($id, $p);
422
        $parents = $request->getParents();
423
424
        if (true === $self && $request instanceof Collection) {
425
            $result[] = $this->node_decorator->decorate($request, $attributes);
426
        }
427
428
        foreach ($parents as $node) {
429
            $result[] = $this->node_decorator->decorate($node, $attributes);
430
        }
431
432
        return (new Response())->setCode(200)->setBody($result);
433
    }
434
435
    /**
436
     * @api {patch} /api/v2/nodes/:id Change attributes
437
     * @apiVersion 2.0.0
438
     * @apiName patch
439
     * @apiGroup Node
440
     * @apiPermission none
441
     * @apiDescription Change attributes
442
     * @apiUse _getNodes
443
     * @apiUse _multiError
444
     *
445
     * @apiParam (GET Parameter) {string} [name] Rename node, the characters (\ < > : " / * ? |) (without the "()") are not allowed to use within a node name.
446
     * @apiParam (GET Parameter) {boolean} [readonly] Mark node as readonly
447
     * @apiParam (GET Parameter) {object} [filter] Custom collection filter (Collection only)
448
     * @apiParam (GET Parameter) {string} [meta.description] UTF-8 Text Description - Can contain anything as long as it is a string
449
     * @apiParam (GET Parameter) {string} [meta.color] Color Tag - Can contain anything as long as it is a string
450
     * @apiParam (GET Parameter) {string} [meta.author] Author - Can contain anything as long as it is a string
451
     * @apiParam (GET Parameter) {string} [meta.mail] Mail contact address - Can contain anything as long as it is a string
452
     * @apiParam (GET Parameter) {string} [meta.license] License - Can contain anything as long as it is a string
453
     * @apiParam (GET Parameter) {string} [meta.copyright] Copyright string - Can contain anything as long as it is a string
454
     * @apiParam (GET Parameter) {string[]} [meta.tags] Tags - Must be an array full of strings
455
     *
456
     * @apiExample (cURL) example:
457
     * curl -XPATCH "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686?name=example"
458
     *
459
     * @apiSuccessExample {json} Success-Response:
460
     * HTTP/1.1 200
461
     *
462
     * @param array|string $id
463
     * @param array|string $p
464
     */
465
    public function patch(?string $name = null, ?array $meta = null, ?bool $readonly = null, ?array $filter = null, ?array $acl = null, ?string $id = null, ?string $p = null): Response
466
    {
467
        $attributes = compact('name', 'meta', 'readonly', 'filter', 'acl');
468
        $attributes = array_filter($attributes, function ($attribute) {return !is_null($attribute); });
469
470
        return $this->bulk($id, $p, function ($node) use ($attributes) {
471
            foreach ($attributes as $attribute => $value) {
472
                switch ($attribute) {
473
                    case 'name':
474
                        $node->setName($value);
475
476
                    break;
477
                    case 'meta':
478
                        $node->setMetaAttributes($value);
479
480
                    break;
481
                    case 'readonly':
482
                        $node->setReadonly($value);
483
484
                    break;
485
                    case 'filter':
486
                        if ($node instanceof Collection) {
487
                            $node->setFilter($value);
488
                        }
489
490
                    break;
491
                    case 'acl':
492
                        $node->setAcl($value);
493
494
                    break;
495
                }
496
            }
497
498
            return [
499
                'code' => 200,
500
                'data' => $this->node_decorator->decorate($node),
501
            ];
502
        });
503
    }
504
505
    /**
506
     * @api {post} /api/v2/nodes/:id/clone Clone node
507
     * @apiVersion 2.0.0
508
     * @apiName postClone
509
     * @apiGroup Node
510
     * @apiPermission none
511
     * @apiDescription Clone a node
512
     * @apiUse _getNode
513
     * @apiUse _conflictNode
514
     * @apiUse _multiError
515
     * @apiUse _writeAction
516
     *
517
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
518
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
519
     *
520
     * @apiExample (cURL) example:
521
     * curl -XPOST "https://SERVER/api/v2/nodes/clone?id=544627ed3c58891f058b4686&dest=544627ed3c58891f058b4676"
522
     * curl -XPOST "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686/clone?dest=544627ed3c58891f058b4676&conflict=2"
523
     * curl -XPOST "https://SERVER/api/v2/nodes/clone?p=/absolute/path/to/my/node&conflict=0&destp=/new/parent"
524
     *
525
     * @apiSuccessExample {json} Success-Response:
526
     * HTTP/1.1 201 Created
527
     *
528
     * @apiSuccessExample {json} Success-Response:
529
     * HTTP/1.1 200 OK
530
     *
531
     * @param array|string $id
532
     * @param array|string $id
533
     * @param array|string $p
534
     * @param string       $destid
535
     * @param string       $destp
536
     */
537
    public function postClone(
538
        $id = null,
539
        $p = null,
540
        ?string $destid = null,
541
        ?string $destp = null,
542
        int $conflict = 0
543
    ): Response {
544
        try {
545
            $parent = $this->_getNode($destid, $destp, Collection::class, false, true);
546
        } catch (Exception\NotFound $e) {
547
            throw new Exception\NotFound(
548
                'destination collection was not found or is not a collection',
549
                Exception\NotFound::DESTINATION_NOT_FOUND
550
            );
551
        }
552
553
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict) {
554
            $result = $node->copyTo($parent, $conflict);
555
556
            return [
557
                'code' => $parent == $result ? 200 : 201,
558
                'data' => $this->node_decorator->decorate($result),
559
            ];
560
        });
561
    }
562
563
    /**
564
     * @api {post} /api/v2/nodes/:id/move Move node
565
     * @apiVersion 2.0.0
566
     * @apiName postMove
567
     * @apiGroup Node
568
     * @apiPermission none
569
     * @apiDescription Move node
570
     * @apiUse _getNodes
571
     * @apiUse _conflictNode
572
     * @apiUse _multiError
573
     * @apiUse _writeAction
574
     *
575
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
576
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
577
     *
578
     * @apiExample (cURL) example:
579
     * curl -XPOST "https://SERVER/api/v2/nodes/move?id=544627ed3c58891f058b4686?destid=544627ed3c58891f058b4655"
580
     * curl -XPOST "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686/move?destid=544627ed3c58891f058b4655"
581
     * curl -XPOST "https://SERVER/api/v2/nodes/move?p=/absolute/path/to/my/node&destp=/new/parent&conflict=1
582
     *
583
     * @apiSuccessExample {json} Success-Response:
584
     * HTTP/1.1 204 No Content
585
     *
586
     * @apiSuccessExample {json} Success-Response (conflict=1):
587
     * HTTP/1.1 200 OK
588
     * {
589
     *      "status":200,
590
     *      "data": "renamed (xy23)"
591
     * }
592
     *
593
     * @param array|string $id
594
     * @param array|string $p
595
     * @param string       $destid
596
     * @param string       $destp
597
     */
598
    public function postMove(
599
        $id = null,
600
        $p = null,
601
        ?string $destid = null,
602
        ?string $destp = null,
603
        int $conflict = 0
604
    ): Response {
605
        try {
606
            $parent = $this->_getNode($destid, $destp, Collection::class, false, true);
607
        } catch (Exception\NotFound $e) {
608
            throw new Exception\NotFound(
609
                'destination collection was not found or is not a collection',
610
                Exception\NotFound::DESTINATION_NOT_FOUND
611
            );
612
        }
613
614
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict) {
615
            $result = $node->setParent($parent, $conflict);
616
617
            return [
618
                'code' => 200,
619
                'data' => $this->node_decorator->decorate($node),
620
            ];
621
        });
622
    }
623
624
    /**
625
     * @api {delete} /api/v2/nodes/:id Delete node
626
     * @apiVersion 2.0.0
627
     * @apiName delete
628
     * @apiGroup Node
629
     * @apiPermission none
630
     * @apiDescription Delete node
631
     * @apiUse _getNodes
632
     * @apiUse _multiError
633
     * @apiUse _writeAction
634
     *
635
     * @apiParam (GET Parameter) {boolean} [force=false] Force flag need to be set to delete a node from trash (node must have the deleted flag)
636
     * @apiParam (GET Parameter) {boolean} [ignore_flag=false] If both ignore_flag and force_flag were set, the node will be deleted completely
637
     * @apiParam (GET Parameter) {number} [at] Has to be a valid unix timestamp if so the node will destroy itself at this specified time instead immediatly
638
     *
639
     * @apiExample (cURL) example:
640
     * curl -XDELETE "https://SERVER/api/v2/node?id=544627ed3c58891f058b4686"
641
     * curl -XDELETE "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686?force=1&ignore_flag=1"
642
     * curl -XDELETE "https://SERVER/api/v2/node?p=/absolute/path/to/my/node"
643
     *
644
     * @apiSuccessExample {json} Success-Response:
645
     * HTTP/1.1 204 No Content
646
     *
647
     * @param array|string $id
648
     * @param array|string $p
649
     * @param int          $at
650
     */
651
    public function delete(
652
        $id = null,
653
        $p = null,
654
        bool $force = false,
655
        bool $ignore_flag = false,
656
        ?string $at = null
657
    ): Response {
658
        $failures = [];
659
660
        if (null !== $at && '0' !== $at) {
661
            $at = $this->_verifyAttributes(['destroy' => $at])['destroy'];
662
        }
663
664
        return $this->bulk($id, $p, function ($node) use ($force, $ignore_flag, $at) {
665
            if (null === $at) {
666
                $node->delete($force && $node->isDeleted() || $force && $ignore_flag);
667
            } else {
668
                if ('0' === $at) {
669
                    $at = null;
670
                }
671
                $node->setDestroyable($at);
672
            }
673
674
            return [
675
                'code' => 204,
676
            ];
677
        });
678
    }
679
680
    /**
681
     * @api {get} /api/v2/nodes/trash Get trash
682
     * @apiName getTrash
683
     * @apiVersion 2.0.0
684
     * @apiGroup Node
685
     * @apiPermission none
686
     * @apiDescription A similar endpoint to /api/v2/nodes/query filer={'deleted': {$type: 9}] but instead returning all deleted
687
     * nodes (including children which are deleted as well) this enpoint only returns the first deleted node from every subtree)
688
     * @apiUse _nodeAttributes
689
     *
690
     * @apiExample (cURL) example:
691
     * curl -XGET https://SERVER/api/v2/nodes/trash?pretty
692
     *
693
     * @apiParam (GET Parameter) {string[]} [attributes] Filter node attributes
694
     *
695
     * @apiSuccess (200 OK) {object[]} - List of deleted nodes
696
     * @apiSuccessExample {json} Success-Response:
697
     * HTTP/1.1 200 OK
698
     * [
699
     *  {
700
     *  }
701
     * ]
702
     */
703
    public function getTrash(array $attributes = [], int $offset = 0, int $limit = 20): Response
704
    {
705
        $children = [];
706
        $nodes = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_ONLY, ['deleted' => ['$type' => 9]], $offset, $limit);
707
708
        foreach ($nodes as $node) {
709
            try {
710
                $parent = $node->getParent();
711
                if (null !== $parent && $parent->isDeleted()) {
712
                    continue;
713
                }
714
            } catch (\Exception $e) {
715
                //skip exception
716
            }
717
718
            $children[] = $node;
719
        }
720
721
        if ($this instanceof ApiFile) {
722
            $query['directory'] = false;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$query was never initialized. Although not strictly required by PHP, it is generally a good practice to add $query = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
723
            $uri = '/api/v2/files';
724
        } elseif ($this instanceof ApiCollection) {
725
            $query['directory'] = true;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$query was never initialized. Although not strictly required by PHP, it is generally a good practice to add $query = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
726
            $uri = '/api/v2/collections';
727
        } else {
728
            $uri = '/api/v2/nodes';
729
        }
730
731
        $pager = new Pager($this->node_decorator, $children, $attributes, $offset, $limit, $uri, $nodes->getReturn());
732
        $result = $pager->paging();
733
734
        return (new Response())->setCode(200)->setBody($result);
735
    }
736
737
    /**
738
     * @api {get} /api/v2/nodes/delta Get delta
739
     * @apiVersion 2.0.0
740
     * @apiName getDelta
741
     * @apiGroup Node
742
     * @apiPermission none
743
     * @apiUse _getNode
744
     *
745
     * @apiDescription Use this method to request a delta feed with all changes on the server (or) a snapshot of the server state.
746
     * since the state of the submited cursor. If no cursor was submited the server will create one which can than be used to request any further deltas.
747
     * If has_more is TRUE you need to request /delta immediatly again to
748
     * receive the next bunch of deltas. If has_more is FALSE you should wait at least 120s seconds before any further requests to the
749
     * api endpoint. You can also specify additional node attributes with the $attributes paramter or request the delta feed only for a specific node (see Get Attributes for that).
750
     * If reset is TRUE you have to clean your local state because you will receive a snapshot of the server state, it is the same as calling the /delta endpoint
751
     * without a cursor. reset could be TRUE if there was an account maintenance or a simialar case.
752
     * You can request a different limit as well but be aware that the number of nodes could be slighty different from your requested limit.
753
     * If requested with parameter id or p the delta gets generated recursively from the node given.
754
     *
755
     * @apiParam (GET Parameter) {number} [limit=250] Limit the number of delta entries, if too low you have to call this endpoint more often since has_more would be true more often
756
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default not all attributes would be returned
757
     * @apiParam (GET Parameter) {string} [cursor=null] Set a cursor to rquest next nodes within delta processing
758
     *
759
     * @apiExample (cURL) example:
760
     * curl -XGET "https://SERVER/api/v2/nodes/delta?pretty"
761
     *
762
     * @apiSuccess (200 OK) {boolean} reset If true the local state needs to be reseted, is alway TRUE during
763
     * the first request to /delta without a cursor or in special cases like server or account maintenance
764
     * @apiSuccess (200 OK) {string} cursor The cursor needs to be stored and reused to request further deltas
765
     * @apiSuccess (200 OK) {boolean} has_more If has_more is TRUE /delta can be requested immediatly after the last request
766
     * to receive further delta. If it is FALSE we should wait at least 120 seconds before any further delta requests to the api endpoint
767
     * @apiSuccess (200 OK) {object[]} nodes Node list to process
768
     * @apiSuccessExample {json} Success-Response:
769
     * HTTP/1.1 200 OK
770
     * {
771
     *      "reset": false,
772
     *      "cursor": "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw==",
773
     *      "has_more": false,
774
     *       "nodes": [
775
     *          {
776
     *              "id": "581afa783c5889ad7c8b4572",
777
     *              "deleted": " 2008-08-31T16:47+00:00",
778
     *              "changed": "2007-08-31T16:47+00:00",
779
     *              "path": "\/AAA\/AL",
780
     *              "directory": true
781
     *          },
782
     *          {
783
     *              "id": "581afa783c5889ad7c8b3dcf",
784
     *              "created": "2007-08-31T16:47+00:00",
785
     *              "changed": "2007-09-28T12:33+00:00",
786
     *              "path": "\/AL",
787
     *              "directory": true
788
     *          }
789
     *      ]
790
     * }
791
     *
792
     * @param string $id
793
     * @param string $p
794
     * @param string $cursor
795
     */
796
    public function getDelta(
797
        DeltaAttributeDecorator $delta_decorator,
798
        ?string $id = null,
799
        ?string $p = null,
800
        ?string $cursor = null,
801
        int $limit = 250,
802
        array $attributes = []
803
    ): Response {
804
        if (null !== $id || null !== $p) {
805
            $node = $this->_getNode($id, $p);
806
        } else {
807
            $node = null;
808
        }
809
810
        $result = $this->fs->getDelta()->getDeltaFeed($cursor, $limit, $node);
811
        foreach ($result['nodes'] as &$node) {
812
            if ($node instanceof NodeInterface) {
813
                $node = $this->node_decorator->decorate($node, $attributes);
814
            } else {
815
                $node = $delta_decorator->decorate($node, $attributes);
816
            }
817
        }
818
819
        return (new Response())->setCode(200)->setBody($result);
820
    }
821
822
    /**
823
     * @api {get} /api/v2/nodes/:id/event-log Event log
824
     * @apiVersion 2.0.0
825
     * @apiName getEventLog
826
     * @apiGroup Node
827
     * @apiPermission none
828
     * @apiUse _getNode
829
     * @apiDescription Get detailed event log
830
     * Request all modifications which are made by the user himself or share members.
831
     * Possible operations are the follwing:
832
     * - deleteCollectionReference
833
     * - deleteCollectionShare
834
     * - deleteCollection
835
     * - addCollection
836
     * - addFile
837
     * - addCollectionShare
838
     * - addCollectionReference
839
     * - undeleteFile
840
     * - undeleteCollectionReference
841
     * - undeleteCollectionShare
842
     * - restoreFile
843
     * - renameFile
844
     * - renameCollection
845
     * - renameCollectionShare
846
     * - renameCollectionRFeference
847
     * - copyFile
848
     * - copyCollection
849
     * - copyCollectionShare
850
     * - copyCollectionRFeference
851
     * - moveFile
852
     * - moveCollection
853
     * - moveCollectionReference
854
     * - moveCollectionShare
855
     *
856
     * @apiExample (cURL) example:
857
     * curl -XGET "https://SERVER/api/v2/nodes/event-log?pretty"
858
     * curl -XGET "https://SERVER/api/v2/nodes/event-log?id=544627ed3c58891f058b4686&pretty"
859
     * curl -XGET "https://SERVER/api/v2/nodes/544627ed3c58891f058b4686/event-log?pretty&limit=10"
860
     * curl -XGET "https://SERVER/api/v2/nodes/event-log?p=/absolute/path/to/my/node&pretty"
861
     *
862
     * @apiParam (GET Parameter) {number} [limit=100] Sets limit of events to be returned
863
     * @apiParam (GET Parameter) {number} [skip=0] How many events are skiped (useful for paging)
864
     *
865
     * @apiSuccess (200 OK) {object[]} - List of events
866
     * @apiSuccess (200 OK) {number} -.event Event ID
867
     * @apiSuccess (200 OK) {object} -.timestamp ISO8601 timestamp when the event occured
868
     * @apiSuccess (200 OK) {string} -.operation event operation (like addCollection, deleteFile, ...)
869
     * @apiSuccess (200 OK) {object} -.parent Parent node object at the time of the event
870
     * @apiSuccess (200 OK) {object} -.previous Previous state of actual data which has been modified during an event, can contain either version, name or parent
871
     * @apiSuccess (200 OK) {number} -.previous.version Version at the time before the event
872
     * @apiSuccess (200 OK) {string} -.previous.name Name at the time before the event
873
     * @apiSuccess (200 OK) {object} -.previous.parent Parent node object at the time before the event
874
     * @apiSuccess (200 OK) {object} -.share shared collection object at the time of the event (If the node was part of a share)
875
     * @apiSuccess (200 OK) {string} -.name Name of the node at the time of the event
876
     * @apiSuccess (200 OK) {object} -.node Current node object
877
     * @apiSuccess (200 OK) {object} -.user User who executed an event
878
     *
879
     * @apiSuccessExample {json} Success-Response:
880
     * HTTP/1.1 200 OK
881
     * [
882
     *  {
883
     *      "id": "57628e523c5889026f8b4570",
884
     *      "timestamp": " 2018-01-02T13:22+00:00",
885
     *      "operation": "restoreFile",
886
     *      "name": "file.txt",
887
     *      "previous": {
888
     *          "version": 16
889
     *      },
890
     *      "node": {
891
     *          "id": "558c0b273c588963078b457a",
892
     *          "name": "3dddsceheckfile.txt",
893
     *          "deleted": false
894
     *      },
895
     *      "parent": null,
896
     *      "user": {
897
     *          "id": "54354cb63c58891f058b457f",
898
     *          "username": "example"
899
     *      }
900
     *  }
901
     * ]
902
     *
903
     * @param string $id
904
     * @param string $p
905
     */
906
    public function getEventLog(EventAttributeDecorator $event_decorator, ?string $id = null, ?string $p = null, ?array $attributes = [], int $offset = 0, int $limit = 20): Response
907
    {
908
        if (null !== $id || null !== $p) {
909
            $node = $this->_getNode($id, $p);
910
            $uri = '/api/v2/nodes/'.$node->getId().'/event-log';
911
        } else {
912
            $node = null;
913
            $uri = '/api/v2/nodes/event-log';
914
        }
915
916
        $result = $this->fs->getDelta()->getEventLog($limit, $offset, $node, $total);
917
        $pager = new Pager($event_decorator, $result, $attributes, $offset, $limit, $uri, $total);
918
919
        return (new Response())->setCode(200)->setBody($pager->paging());
920
    }
921
922
    /**
923
     * @api {get} /api/v2/nodes/last-cursor Get last Cursor
924
     * @apiVersion 2.0.0
925
     * @apiName geLastCursor
926
     * @apiGroup Node
927
     * @apiUse _getNode
928
     * @apiPermission none
929
     * @apiDescription Use this method to request the latest cursor if you only need to now
930
     * if there are changes on the server. This method will not return any other data than the
931
     * newest cursor. To request a feed with all deltas request /delta.
932
     *
933
     * @apiExample (cURL) example:
934
     * curl -XGET "https://SERVER/api/v2/nodes/last-cursor?pretty"
935
     *
936
     * @apiSuccess (200 OK) {string} cursor v2 cursor
937
     * @apiSuccessExample {json} Success-Response:
938
     * HTTP/1.1 200 OK
939
     * "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw=="
940
     *
941
     * @param string $id
942
     * @param string $p
943
     */
944
    public function getLastCursor(?string $id = null, ?string $p = null): Response
945
    {
946
        if (null !== $id || null !== $p) {
947
            $node = $this->_getNode($id, $p);
948
        } else {
949
            $node = null;
950
        }
951
952
        $result = $this->fs->getDelta()->getLastCursor();
953
954
        return (new Response())->setCode(200)->setBody($result);
955
    }
956
957
    /**
958
     * Merge multiple nodes into one zip archive.
959
     *
960
     * @param string $id
961
     * @param string $path
962
     */
963
    protected function combine($id = null, $path = null, string $name = 'selected')
964
    {
965
        $archive = new ZipStream($name.'.zip');
966
967
        foreach ($this->_getNodes($id, $path) as $node) {
968
            try {
969
                $node->zip($archive);
970
                //json_decode($stored, true),
971
            } catch (\Exception $e) {
972
                $this->logger->debug('failed zip node in multi node request ['.$node->getId().']', [
973
                   'category' => get_class($this),
974
                   'exception' => $e,
975
               ]);
976
            }
977
        }
978
979
        $archive->finish();
980
    }
981
982
    /**
983
     * Check custom node attributes which have to be written.
984
     */
985
    protected function _verifyAttributes(array $attributes): array
986
    {
987
        $valid_attributes = [
988
            'changed',
989
            'destroy',
990
            'created',
991
            'meta',
992
            'readonly',
993
            'acl',
994
        ];
995
996
        if ($this instanceof ApiCollection) {
997
            $valid_attributes += ['filter', 'mount'];
998
        }
999
1000
        $check = array_merge(array_flip($valid_attributes), $attributes);
1001
1002
        if ($this instanceof ApiCollection && count($check) > 8) {
1003
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, filter, mount, readonly and/or meta attributes may be overwritten');
1004
        }
1005
        if ($this instanceof ApiFile && count($check) > 6) {
1006
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, readonly and/or meta attributes may be overwritten');
1007
        }
1008
1009
        foreach ($attributes as $attribute => $value) {
1010
            switch ($attribute) {
1011
                case 'filter':
1012
                    if (!is_array($value)) {
1013
                        throw new Exception\InvalidArgument($attribute.' must be an array');
1014
                    }
1015
1016
                    $attributes['filter'] = json_encode($value);
1017
1018
                break;
1019
                case 'destroy':
1020
                    if (!Helper::isValidTimestamp($value)) {
1021
                        throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp');
1022
                    }
1023
                    $attributes[$attribute] = new UTCDateTime($value.'000');
1024
1025
                break;
1026
                case 'changed':
1027
                case 'created':
1028
                    if (!Helper::isValidTimestamp($value)) {
1029
                        throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp');
1030
                    }
1031
                    if ((int) $value > time()) {
1032
                        throw new Exception\InvalidArgument($attribute.' timestamp can not be set greater than the server time');
1033
                    }
1034
                    $attributes[$attribute] = new UTCDateTime($value.'000');
1035
1036
                break;
1037
                case 'readonly':
1038
                    $attributes['readonly'] = (bool) $attributes['readonly'];
1039
1040
                break;
1041
            }
1042
        }
1043
1044
        return $attributes;
1045
    }
1046
}
1047