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

src/app/Balloon.App.Api/v1/Node.php (1 issue)

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\v1;
13
14
use Balloon\App\Api\Controller;
15
use Balloon\App\Api\v1\AttributeDecorator\DeltaDecorator;
16
use Balloon\App\Api\v1\AttributeDecorator\EventDecorator;
17
use Balloon\App\Api\v1\AttributeDecorator\NodeDecorator;
18
use Balloon\App\Api\v1\Collection as ApiCollection;
19
use Balloon\App\Api\v1\File as ApiFile;
20
use Balloon\Filesystem;
21
use Balloon\Filesystem\Exception;
22
use Balloon\Filesystem\Node\Collection;
23
use Balloon\Filesystem\Node\File;
24
use Balloon\Filesystem\Node\NodeInterface;
25
use Balloon\Helper;
26
use Balloon\Server;
27
use Balloon\Server\User;
28
use Closure;
29
use Generator;
30
use Micro\Http\Response;
31
use MongoDB\BSON\UTCDateTime;
32
use MongoDB\Database;
33
use Psr\Log\LoggerInterface;
34
use ZipStream\ZipStream;
35
36
class Node extends Controller
37
{
38
    /**
39
     * Filesystem.
40
     *
41
     * @var Filesystem
42
     */
43
    protected $fs;
44
45
    /**
46
     * LoggerInterface.
47
     *
48
     * @var LoggerInterface
49
     */
50
    protected $logger;
51
52
    /**
53
     * Server.
54
     *
55
     * @var Server
56
     */
57
    protected $server;
58
59
    /**
60
     * User.
61
     *
62
     * @var User
63
     */
64
    protected $user;
65
66
    /**
67
     * Decorator.
68
     *
69
     * @var NodeDecorator
70
     */
71
    protected $node_decorator;
72
73
    /**
74
     * Database.
75
     *
76
     * @var Database
77
     */
78
    protected $db;
79
80
    /**
81
     * Initialize.
82
     */
83
    public function __construct(Server $server, NodeDecorator $decorator, LoggerInterface $logger, Database $db)
84
    {
85
        $this->fs = $server->getFilesystem();
86
        $this->user = $server->getIdentity();
87
        $this->server = $server;
88
        $this->node_decorator = $decorator;
89
        $this->logger = $logger;
90
        $this->db = $db;
91
    }
92
93
    /**
94
     * @api {head} /api/v1/node?id=:id Node exists?
95
     * @apiVersion 1.0.0
96
     * @apiName head
97
     * @apiGroup Node
98
     * @apiPermission none
99
     * @apiDescription Check if a node exists. Per default deleted nodes are ignore which means it will
100
     *  return a 404 if a deleted node is requested. You can change this behaviour via the deleted parameter.
101
     * @apiUse _getNode
102
     *
103
     * @apiExample (cURL) example:
104
     * curl -XHEAD "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686"
105
     * curl -XHEAD "https://SERVER/api/v1/node/544627ed3c58891f058b4686"
106
     * curl -XHEAD "https://SERVER/api/v1/node?p=/absolute/path/to/my/node"
107
     *
108
     * @apiParam (GET Parameter) {number} [deleted=0] Wherever include deleted node or not, possible values:</br>
109
     * - 0 Exclude deleted</br>
110
     * - 1 Only deleted</br>
111
     * - 2 Include deleted</br>
112
     *
113
     * @apiSuccessExample {json} Success-Response (Node does exist):
114
     * HTTP/1.1 200 OK
115
     *
116
     * @apiSuccessExample {json} Success-Response (Node does not exist):
117
     * HTTP/1.1 404 Not Found
118
     *
119
     * @param string $id
120
     * @param string $p
121
     */
122
    public function head(?string $id = null, ?string $p = null, int $deleted = 0): Response
123
    {
124
        try {
125
            $result = $this->_getNode($id, $p, null, false, false, $deleted);
126
127
            $response = (new Response())
128
                ->setHeader('Content-Length', (string) $result->getSize())
129
                ->setHeader('Content-Type', $result->getContentType())
130
                ->setCode(200);
131
132
            return $response;
133
        } catch (\Exception $e) {
134
            return (new Response())->setCode(404);
135
        }
136
    }
137
138
    /**
139
     * @api {post} /api/v1/node/undelete?id=:id Undelete node
140
     * @apiVersion 1.0.0
141
     * @apiName postUndelete
142
     * @apiGroup Node
143
     * @apiPermission none
144
     * @apiDescription Undelete (Apiore from trash) a single node or multiple ones.
145
     * @apiUse _getNodes
146
     * @apiUse _conflictNode
147
     * @apiUse _multiError
148
     * @apiUse _writeAction
149
     *
150
     * @apiExample (cURL) example:
151
     * curl -XPOST "https://SERVER/api/v1/node/undelete?id[]=544627ed3c58891f058b4686&id[]=544627ed3c58891f058b46865&pretty"
152
     * curl -XPOST "https://SERVER/api/v1/node/undelete?id=544627ed3c58891f058b4686?pretty"
153
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/undelete?conflict=2"
154
     * curl -XPOST "https://SERVER/api/v1/node/undelete?p=/absolute/path/to/my/node&conflict=0&move=1&destid=544627ed3c58891f058b46889"
155
     *
156
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
157
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
158
     *
159
     * @apiSuccessExample {json} Success-Response:
160
     * HTTP/1.1 204 No Content
161
     *
162
     * @apiSuccessExample {json} Success-Response (conflict=1):
163
     * HTTP/1.1 200 OK
164
     * {
165
     *      "status":200,
166
     *      "data": "renamed (xy23)"
167
     *      }
168
     * }
169
     *
170
     * @param array|string $id
171
     * @param array|string $p
172
     * @param string       $destid
173
     * @param string       $destp
174
     */
175
    public function postUndelete(
176
        $id = null,
177
        $p = null,
178
        bool $move = false,
179
        ?string $destid = null,
180
        ?string $destp = null,
181
        int $conflict = 0
182
    ): Response {
183
        $parent = null;
184
        if (true === $move) {
185
            try {
186
                $parent = $this->_getNode($destid, $destp, 'Collection', false, true);
187
            } catch (Exception\NotFound $e) {
188
                throw new Exception\NotFound(
189
                    'destination collection was not found or is not a collection',
190
                    Exception\NotFound::DESTINATION_NOT_FOUND
191
                );
192
            }
193
        }
194
195
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict, $move) {
196
            if (true === $move) {
197
                $node = $node->setParent($parent, $conflict);
198
            }
199
200
            if ($node->isDeleted()) {
201
                $node->undelete($conflict);
202
            }
203
204
            if (true === $move && NodeInterface::CONFLICT_RENAME === $conflict) {
205
                return [
206
                    'status' => 200,
207
                    'data' => [
208
                    ],
209
                ];
210
            }
211
212
            return ['status' => 204];
213
        });
214
    }
215
216
    /**
217
     * @api {get} /api/v1/node?id=:id Download stream
218
     * @apiVersion 1.0.0
219
     * @apiName get
220
     * @apiGroup Node
221
     * @apiPermission none
222
     * @apiDescription Download node contents. Collections (Folder) are converted into
223
     * a zip file in realtime.
224
     * @apiUse _getNode
225
     *
226
     * @apiParam (GET Parameter) {number} [offset=0] Get content from a specific offset in bytes
227
     * @apiParam (GET Parameter) {number} [length=0] Get content with until length in bytes reached
228
     * @apiParam (GET Parameter) {string} [encode] Can be set to base64 to encode content as base64.
229
     * @apiParam (GET Parameter) {boolean} [download=false] Force download file (Content-Disposition: attachment HTTP header)
230
     *
231
     * @apiExample (cURL) example:
232
     * curl -XGET "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686" > myfile.txt
233
     * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686" > myfile.txt
234
     * curl -XGET "https://SERVER/api/v1/node?p=/absolute/path/to/my/collection" > folder.zip
235
     *
236
     * @apiSuccessExample {string} Success-Response (encode=base64):
237
     * HTTP/1.1 200 OK
238
     *
239
     * @apiSuccessExample {binary} Success-Response:
240
     * HTTP/1.1 200 OK
241
     *
242
     * @apiErrorExample {json} Error-Response (Invalid offset):
243
     * HTTP/1.1 400 Bad Request
244
     * {
245
     *      "status": 400,
246
     *      "data": {
247
     *          "error": "Balloon\\Exception\\Conflict",
248
     *          "message": "invalid offset requested",
249
     *          "code": 277
250
     *      }
251
     * }
252
     *
253
     * @param array|string $id
254
     * @param array|string $p
255
     * @param string       $encode
256
     */
257
    public function get(
258
        $id = null,
259
        $p = null,
260
        int $offset = 0,
261
        int $length = 0,
262
        ?string $encode = null,
263
        bool $download = false,
264
        string $name = 'selected'
265
    ): ?Response {
266
        if (is_array($id) || is_array($p)) {
267
            return $this->combine($id, $p, $name);
268
        }
269
270
        $node = $this->_getNode($id, $p);
271
        if ($node instanceof Collection) {
272
            return (new Response())->setBody(function () use ($node) {
273
                $node->getZip();
274
            });
275
        }
276
277
        $response = new Response();
278
279
        if (true === $download) {
280
            $response->setHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\''.rawurlencode($name));
281
            $response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
282
            $response->setHeader('Content-Type', 'application/octet-stream');
283
            $response->setHeader('Content-Length', (string) $node->getSize());
284
            $response->setHeader('Content-Transfer-Encoding', 'binary');
285
        } else {
286
            $response->setHeader('Content-Disposition', 'inline; filename*=UTF-8\'\''.rawurlencode($name));
287
        }
288
289
        return $response->setOutputFormat(null)
290
          ->setBody(function () use ($node, $encode, $offset, $length) {
291
              $mime = $node->getContentType();
292
              $stream = $node->get();
293
              $name = $node->getName();
294
295
              if (null === $stream) {
296
                  return;
297
              }
298
299
              if (0 !== $offset) {
300
                  if (fseek($stream, $offset) === -1) {
301
                      throw new Exception\Conflict(
302
                        'invalid offset requested',
303
                        Exception\Conflict::INVALID_OFFSET
304
                    );
305
                  }
306
              }
307
308
              $read = 0;
309
              header('Content-Type: '.$mime.'');
310
              if ('base64' === $encode) {
311
                  header('Content-Encoding: base64');
312
                  while (!feof($stream)) {
313
                      if (0 !== $length && $read + 8192 > $length) {
314
                          echo base64_encode(fread($stream, $length - $read));
315
                          exit();
316
                      }
317
318
                      echo base64_encode(fread($stream, 8192));
319
                      $read += 8192;
320
                  }
321
              } else {
322
                  while (!feof($stream)) {
323
                      if (0 !== $length && $read + 8192 > $length) {
324
                          echo fread($stream, $length - $read);
325
                          exit();
326
                      }
327
328
                      echo fread($stream, 8192);
329
                      $read += 8192;
330
                  }
331
              }
332
          });
333
    }
334
335
    /**
336
     * @api {post} /api/v1/node/readonly?id=:id Mark node as readonly
337
     * @apiVersion 1.0.0
338
     * @apiName postReadonly
339
     * @apiGroup Node
340
     * @apiPermission none
341
     * @apiDescription Mark (or unmark) node as readonly
342
     * @apiUse _getNodes
343
     * @apiUse _multiError
344
     * @apiUse _writeAction
345
     *
346
     * @apiExample (cURL) example:
347
     * curl -XPOST "https://SERVER/api/v1/node/readonly?id[]=544627ed3c58891f058b4686&id[]=544627ed3c58891f058b46865&readonly=1"
348
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/readonly?readonly=0"
349
     * curl -XPOST "https://SERVER/api/v1/node/readonly?p=/absolute/path/to/my/node"
350
     *
351
     * @apiParam (GET Parameter) {bool} [readonly=true] Set readonly to false to make node writeable again
352
     *
353
     * @apiSuccessExample {json} Success-Response:
354
     * HTTP/1.1 204 No Content
355
     *
356
     * @param array|string $id
357
     * @param array|string $p
358
     */
359
    public function postReadonly($id = null, $p = null, bool $readonly = true): Response
360
    {
361
        return $this->bulk($id, $p, function ($node) use ($readonly) {
362
            $node->setReadonly($readonly);
363
364
            return ['status' => 204];
365
        });
366
    }
367
368
    /**
369
     * @apiDefine _nodeAttributes
370
     *
371
     * @apiSuccess (200 OK) {number} status Status Code
372
     * @apiSuccess (200 OK) {object} data Attributes
373
     * @apiSuccess (200 OK) {string} data.id Unique node id
374
     * @apiSuccess (200 OK) {string} data.name Name
375
     * @apiSuccess (200 OK) {string} data.hash MD5 content checksum (file node only)
376
     * @apiSuccess (200 OK) {object} data.meta Extended meta attributes
377
     * @apiSuccess (200 OK) {string} data.meta.description UTF-8 Text Description
378
     * @apiSuccess (200 OK) {string} data.meta.color Color Tag (HEX) (Like: #000000)
379
     * @apiSuccess (200 OK) {string} data.meta.author Author
380
     * @apiSuccess (200 OK) {string} data.meta.mail Mail contact address
381
     * @apiSuccess (200 OK) {string} data.meta.license License
382
     * @apiSuccess (200 OK) {string} data.meta.copyright Copyright string
383
     * @apiSuccess (200 OK) {string[]} data.meta.tags Search Tags
384
     * @apiSuccess (200 OK) {number} data.size Size in bytes (Only file node), number of children if collection
385
     * @apiSuccess (200 OK) {string} data.mime Mime type
386
     * @apiSuccess (200 OK) {boolean} data.sharelink Is node shared?
387
     * @apiSuccess (200 OK) {number} data.version File version (file node only)
388
     * @apiSuccess (200 OK) {mixed} data.deleted Is boolean false if not deleted, if deleted it contains a deleted timestamp
389
     * @apiSuccess (200 OK) {number} data.deleted.sec Unix timestamp
390
     * @apiSuccess (200 OK) {number} data.deleted.usec Additional Microsecconds to Unix timestamp
391
     * @apiSuccess (200 OK) {object} data.changed Changed timestamp
392
     * @apiSuccess (200 OK) {number} data.changed.sec Unix timestamp
393
     * @apiSuccess (200 OK) {number} data.changed.usec Additional Microsecconds to Unix timestamp
394
     * @apiSuccess (200 OK) {object} data.created Created timestamp
395
     * @apiSuccess (200 OK) {number} data.created.sec Unix timestamp
396
     * @apiSuccess (200 OK) {number} data.created.usec Additional Microsecconds to Unix timestamp
397
     * @apiSuccess (200 OK) {boolean} data.share Node is shared
398
     * @apiSuccess (200 OK) {boolean} data.directory Is node a collection or a file?
399
     *
400
     * @apiSuccess (200 OK - additional attributes) {string} data.thumbnail Id of preview (file node only)
401
     * @apiSuccess (200 OK - additional attributes) {string} data.access Access if node is shared, one of r/rw/w
402
     * @apiSuccess (200 OK - additional attributes) {string} data.shareowner Username of the share owner
403
     * @apiSuccess (200 OK - additional attributes) {string} data.parent ID of the parent node
404
     * @apiSuccess (200 OK - additional attributes) {string} data.path Absolute node path
405
     * @apiSuccess (200 OK - additional attributes) {boolean} data.filtered Node is filtered (usually only a collection)
406
     * @apiSuccess (200 OK - additional attributes) {boolean} data.readonly Node is readonly
407
     *
408
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default not all attributes would be returned
409
     *
410
     * @param null|mixed $id
411
     * @param null|mixed $p
412
     */
413
414
    /**
415
     * @api {get} /api/v1/node/attributes?id=:id Get attributes
416
     * @apiVersion 1.0.0
417
     * @apiName getAttributes
418
     * @apiGroup Node
419
     * @apiPermission none
420
     * @apiDescription Get attributes from one or multiple nodes
421
     * @apiUse _getNode
422
     * @apiUse _nodeAttributes
423
     *
424
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default only a bunch of attributes would be returned, if you
425
     * need other attributes you have to request them (for example "path")
426
     *
427
     * @apiExample (cURL) example:
428
     * curl -XGET "https://SERVER/api/v1/node/attributes?id=544627ed3c58891f058b4686&pretty"
429
     * curl -XGET "https://SERVER/api/v1/node/attributes?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty"
430
     * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/attributes?pretty"
431
     * curl -XGET "https://SERVER/api/v1/node/attributes?p=/absolute/path/to/my/node&pretty"
432
     *
433
     * @apiSuccessExample {json} Success-Response:
434
     * HTTP/1.1 200 OK
435
     * {
436
     *     "status": 200,
437
     *     "data": {
438
     *          "id": "544627ed3c58891f058b4686",
439
     *          "name": "api.php",
440
     *          "hash": "a77f23ed800fd7a600a8c2cfe8cc370b",
441
     *          "meta": {
442
     *              "license": "GPLv3"
443
     *          },
444
     *          "size": 178,
445
     *          "mime": "text\/plain",
446
     *          "sharelink": true,
447
     *          "version": 1,
448
     *          "deleted": false,
449
     *          "changed": {
450
     *              "sec": 1413883885,
451
     *              "usec": 869000
452
     *          },
453
     *          "created": {
454
     *              "sec": 1413883885,
455
     *              "usec": 869000
456
     *          },
457
     *          "share": false,
458
     *          "directory": false
459
     *      }
460
     * }
461
     *
462
     * @param array|string $id
463
     * @param array|string $p
464
     */
465
    public function getAttributes($id = null, $p = null, array $attributes = []): Response
466
    {
467
        if (is_array($id) || is_array($p)) {
468
            $nodes = [];
469
            foreach ($this->_getNodes($id, $p) as $node) {
470
                $nodes[] = $this->node_decorator->decorate($node, $attributes);
471
            }
472
473
            return (new Response())->setCode(200)->setBody([
474
                'status' => 200,
475
                'data' => $nodes,
476
            ]);
477
        }
478
479
        $result = $this->node_decorator->decorate($this->_getNode($id, $p), $attributes);
480
481
        return (new Response())->setCode(200)->setBody([
482
            'status' => 200,
483
            'data' => $result,
484
        ]);
485
    }
486
487
    /**
488
     * @api {get} /api/v1/node/parents?id=:id Get parent nodes
489
     * @apiVersion 1.0.0
490
     * @apiName getParents
491
     * @apiGroup Node
492
     * @apiPermission none
493
     * @apiDescription Get system attributes of all parent nodes. The hirarchy of all parent nodes is ordered in a
494
     * single level array beginning with the collection on the highest level.
495
     * @apiUse _getNode
496
     * @apiUse _nodeAttributes
497
     * @apiSuccess (200 OK) {object[]} data Nodes
498
     *
499
     * @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)
500
     *
501
     * @apiExample (cURL) example:
502
     * curl -XGET "https://SERVER/api/v1/node/parents?id=544627ed3c58891f058b4686&pretty"
503
     * curl -XGET "https://SERVER/api/v1/node/parents?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty"
504
     * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/parents?pretty&self=1"
505
     * curl -XGET "https://SERVER/api/v1/node/parents?p=/absolute/path/to/my/node&self=1"
506
     *
507
     * @apiSuccessExample {json} Success-Response:
508
     * HTTP/1.1 200 OK
509
     * {
510
     *     "status": 200,
511
     *     "data": [
512
     * {
513
     *              "id": "544627ed3c58891f058bbbaa",
514
     *              "name": "rootdir",
515
     *              "meta": {},
516
     *              "size": 1,
517
     *              "mime": "inode\/directory",
518
     *              "deleted": false,
519
     *              "changed": {
520
     *                  "sec": 1413883880,
521
     *                  "usec": 869001
522
     *              },
523
     *              },
524
     *              "created": {
525
     *                  "sec": 1413883880,
526
     *                  "usec": 869001
527
     *              },
528
     *              "share": false,
529
     *              "directory": true
530
     *          },
531
     *          {
532
     *              "id": "544627ed3c58891f058b46cc",
533
     *              "name": "parentdir",
534
     *              "meta": {},
535
     *              "size": 3,
536
     *              "mime": "inode\/directory",
537
     *              "deleted": false,
538
     *              "changed": {
539
     *                  "sec": 1413883885,
540
     *                  "usec": 869000
541
     *              },
542
     *              "created": {
543
     *                  "sec": 1413883885,
544
     *                  "usec": 869000
545
     *              },
546
     *              "share": false,
547
     *              "directory": true
548
     *          }
549
     *      ]
550
     * }
551
     *
552
     * @param string $id
553
     * @param string $p
554
     */
555
    public function getParents(?string $id = null, ?string $p = null, array $attributes = [], bool $self = false): Response
556
    {
557
        $request = $this->_getNode($id, $p);
558
        $parents = $request->getParents();
559
        $result = [];
560
561
        if (true === $self && $request instanceof Collection) {
562
            $result[] = $this->node_decorator->decorate($request, $attributes);
563
        }
564
565
        foreach ($parents as $node) {
566
            $result[] = $this->node_decorator->decorate($node, $attributes);
567
        }
568
569
        return (new Response())->setCode(200)->setBody([
570
            'status' => 200,
571
            'data' => $result,
572
        ]);
573
    }
574
575
    /**
576
     * @api {post} /api/v1/node/meta-attributes?id=:id Change meta attributes
577
     * @apiVersion 1.0.0
578
     * @apiName postMetaAttributes
579
     * @apiGroup Node
580
     * @apiPermission none
581
     * @apiDescription Change meta attributes of a node
582
     * @apiUse _getNodes
583
     * @apiUse _multiError
584
     *
585
     * @apiParam (GET Parameter) {string} [attributes.description] UTF-8 Text Description - Can contain anything as long as it is a string
586
     * @apiParam (GET Parameter) {string} [attributes.color] Color Tag - Can contain anything as long as it is a string
587
     * @apiParam (GET Parameter) {string} [attributes.author] Author - Can contain anything as long as it is a string
588
     * @apiParam (GET Parameter) {string} [attributes.mail] Mail contact address - Can contain anything as long as it is a string
589
     * @apiParam (GET Parameter) {string} [attributes.license] License - Can contain anything as long as it is a string
590
     * @apiParam (GET Parameter) {string} [attributes.copyright] Copyright string - Can contain anything as long as it is a string
591
     * @apiParam (GET Parameter) {string[]} [attributes.tags] Tags - Must be an array full of strings
592
     *
593
     * @apiExample (cURL) example:
594
     * curl -XPOST "https://SERVER/api/v1/node/meta-attributes?id=544627ed3c58891f058b4686&author=peter.meier"
595
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/meta-attributes?author=example"
596
     * curl -XPOST "https://SERVER/api/v1/node/meta-attributes?p=/absolute/path/to/my/node?license=GPL-3.0"
597
     *
598
     * @apiSuccessExample {json} Success-Response:
599
     * HTTP/1.1 204 No Content
600
     *
601
     * @param array|string $id
602
     * @param array|string $p
603
     */
604
    public function postMetaAttributes(?string $id = null, ?string $p = null): Response
605
    {
606
        return $this->bulk($id, $p, function ($node) {
607
            $node->setMetaAttributes($_POST);
608
609
            return ['status' => 204];
610
        });
611
    }
612
613
    /**
614
     * @api {post} /api/v1/node/name?id=:id Rename node
615
     * @apiVersion 1.0.0
616
     * @apiName postName
617
     * @apiGroup Node
618
     * @apiPermission none
619
     * @apiDescription Rename a node. The characters (\ < > : " / * ? |) (without the "()") are not allowed to use within a node name.
620
     * @apiUse _getNode
621
     * @apiUse _writeAction
622
     *
623
     * @apiParam (GET Parameter) {string} [name] The new name of the node
624
     * @apiError (Error 400) Exception name contains invalid characters
625
     *
626
     * @apiExample (cURL) example:
627
     * curl -XPOST "https://SERVER/api/v1/node/name?id=544627ed3c58891f058b4686&name=newname.txt"
628
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4677/name?name=newdir"
629
     * curl -XPOST "https://SERVER/api/v1/node/name?p=/absolute/path/to/my/node&name=newname.txt"
630
     *
631
     * @apiSuccessExample {json} Success-Response:
632
     * HTTP/1.1 204 No Content
633
     *
634
     * @param string $id
635
     * @param string $p
636
     */
637
    public function postName(string $name, ?string $id = null, ?string $p = null): Response
638
    {
639
        $this->_getNode($id, $p)->setName($name);
640
641
        return (new Response())->setCode(204);
642
    }
643
644
    /**
645
     * @api {post} /api/v1/node/clone?id=:id Clone node
646
     * @apiVersion 1.0.0
647
     * @apiName postClone
648
     * @apiGroup Node
649
     * @apiPermission none
650
     * @apiDescription Clone a node
651
     * @apiUse _getNode
652
     * @apiUse _conflictNode
653
     * @apiUse _multiError
654
     * @apiUse _writeAction
655
     *
656
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
657
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
658
     *
659
     * @apiExample (cURL) example:
660
     * curl -XPOST "https://SERVER/api/v1/node/clone?id=544627ed3c58891f058b4686&dest=544627ed3c58891f058b4676"
661
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/clone?dest=544627ed3c58891f058b4676&conflict=2"
662
     * curl -XPOST "https://SERVER/api/v1/node/clone?p=/absolute/path/to/my/node&conflict=0&destp=/new/parent"
663
     *
664
     * @apiSuccessExample {json} Success-Response:
665
     * HTTP/1.1 204 No Content
666
     *
667
     * @param array|string $id
668
     * @param array|string $p
669
     * @param string       $destid
670
     * @param string       $destp
671
     */
672
    public function postClone(
673
        $id = null,
674
        $p = null,
675
        ?string $destid = null,
676
        ?string $destp = null,
677
        int $conflict = 0
678
    ): Response {
679
        try {
680
            $parent = $this->_getNode($destid, $destp, Collection::class, false, true);
681
        } catch (Exception\NotFound $e) {
682
            throw new Exception\NotFound(
683
                'destination collection was not found or is not a collection',
684
                Exception\NotFound::DESTINATION_NOT_FOUND
685
            );
686
        }
687
688
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict) {
689
            $result = $node->copyTo($parent, $conflict);
690
691
            return [
692
                'status' => 201,
693
                'data' => $result,
694
            ];
695
        });
696
    }
697
698
    /**
699
     * @api {post} /api/v1/node/move?id=:id Move node
700
     * @apiVersion 1.0.0
701
     * @apiName postMove
702
     * @apiGroup Node
703
     * @apiPermission none
704
     * @apiDescription Move node
705
     * @apiUse _getNodes
706
     * @apiUse _conflictNode
707
     * @apiUse _multiError
708
     * @apiUse _writeAction
709
     *
710
     * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given.
711
     * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given.
712
     *
713
     * @apiExample (cURL) example:
714
     * curl -XPOST "https://SERVER/api/v1/node/move?id=544627ed3c58891f058b4686?destid=544627ed3c58891f058b4655"
715
     * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/move?destid=544627ed3c58891f058b4655"
716
     * curl -XPOST "https://SERVER/api/v1/node/move?p=/absolute/path/to/my/node&destp=/new/parent&conflict=1
717
     *
718
     * @apiSuccessExample {json} Success-Response:
719
     * HTTP/1.1 204 No Content
720
     *
721
     * @apiSuccessExample {json} Success-Response (conflict=1):
722
     * HTTP/1.1 200 OK
723
     * {
724
     *      "status":200,
725
     *      "data": "renamed (xy23)"
726
     * }
727
     *
728
     * @param array|string $id
729
     * @param array|string $p
730
     * @param string       $destid
731
     * @param string       $destp
732
     */
733
    public function postMove(
734
        $id = null,
735
        $p = null,
736
        ?string $destid = null,
737
        ?string $destp = null,
738
        int $conflict = 0
739
    ): Response {
740
        try {
741
            $parent = $this->_getNode($destid, $destp, Collection::class, false, true);
742
        } catch (Exception\NotFound $e) {
743
            throw new Exception\NotFound(
744
                'destination collection was not found or is not a collection',
745
                Exception\NotFound::DESTINATION_NOT_FOUND
746
            );
747
        }
748
749
        return $this->bulk($id, $p, function ($node) use ($parent, $conflict) {
750
            $result = $node->setParent($parent, $conflict);
751
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
752
                return [
753
                    'status' => 200,
754
                    'data' => $node->getName(),
755
                ];
756
            }
757
758
            return [
759
                'status' => 204,
760
            ];
761
        });
762
    }
763
764
    /**
765
     * @api {delete} /api/v1/node?id=:id Delete node
766
     * @apiVersion 1.0.0
767
     * @apiName delete
768
     * @apiGroup Node
769
     * @apiPermission none
770
     * @apiDescription Delete node
771
     * @apiUse _getNodes
772
     * @apiUse _multiError
773
     * @apiUse _writeAction
774
     *
775
     * @apiParam (GET Parameter) {boolean} [force=false] Force flag need to be set to delete a node from trash (node must have the deleted flag)
776
     * @apiParam (GET Parameter) {boolean} [ignore_flag=false] If both ignore_flag and force_flag were set, the node will be deleted completely
777
     * @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
778
     *
779
     * @apiExample (cURL) example:
780
     * curl -XDELETE "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686"
781
     * curl -XDELETE "https://SERVER/api/v1/node/544627ed3c58891f058b4686?force=1&ignore_flag=1"
782
     * curl -XDELETE "https://SERVER/api/v1/node?p=/absolute/path/to/my/node"
783
     *
784
     * @apiSuccessExample {json} Success-Response:
785
     * HTTP/1.1 204 No Content
786
     *
787
     * @param array|string $id
788
     * @param array|string $p
789
     * @param int          $at
790
     */
791
    public function delete(
792
        $id = null,
793
        $p = null,
794
        bool $force = false,
795
        bool $ignore_flag = false,
796
        ?string $at = null
797
    ): Response {
798
        $failures = [];
799
800
        if (null !== $at && '0' !== $at) {
801
            $at = $this->_verifyAttributes(['destroy' => $at])['destroy'];
802
        }
803
804
        return $this->bulk($id, $p, function ($node) use ($force, $ignore_flag, $at) {
805
            if (null === $at) {
806
                $node->delete($force && $node->isDeleted() || $force && $ignore_flag);
807
            } else {
808
                if ('0' === $at) {
809
                    $at = null;
0 ignored issues
show
Consider using a different name than the imported variable $at, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
810
                }
811
                $node->setDestroyable($at);
812
            }
813
814
            return [
815
                'status' => 204,
816
            ];
817
        });
818
    }
819
820
    /**
821
     * @api {get} /api/v1/node/query Custom query
822
     * @apiVersion 1.0.0
823
     * @apiName getQuery
824
     * @apiGroup Node
825
     * @apiPermission none
826
     * @apiDescription A custom query is similar requet to children. You do not have to provide any parent node (id or p)
827
     * but you have to provide a filter therefore you can collect any nodes which do match the provided filter. It is a form of a search
828
     * (search) but does not use the search engine like GET /node/search does. You can also create a persistent query collection, just look at
829
     * POST /collection, there you can attach a filter option to the attributes paramater which would be the same as a custom query but just persistent.
830
     * Since query parameters can only be strings and you perhaps would like to filter other data types, you have to send json as parameter to the server.
831
     * @apiUse _nodeAttributes
832
     *
833
     * @apiExample (cURL) example:
834
     * curl -XGET https://SERVER/api/v1/node/query?{%22filter%22:{%22shared%22:true,%22reference%22:{%22$exists%22:0}}}
835
     *
836
     * @apiParam (GET Parameter) {string[]} [attributes] Filter node attributes
837
     * @apiParam (GET Parameter) {string[]} [filter] Filter nodes
838
     * @apiParam (GET Parameter) {number} [deleted=0] Wherever include deleted nodes or not, possible values:</br>
839
     * - 0 Exclude deleted</br>
840
     * - 1 Only deleted</br>
841
     * - 2 Include deleted</br>
842
     *
843
     * @apiSuccess (200 OK) {number} status Status Code
844
     * @apiSuccess (200 OK) {object[]} data Children
845
     * @apiSuccessExample {json} Success-Response:
846
     * HTTP/1.1 200 OK
847
     * {
848
     *      "status":200,
849
     *      "data": [{..}, {...}] //Shorted
850
     * }
851
     */
852
    public function getQuery(int $deleted = 0, array $filter = [], array $attributes = []): Response
853
    {
854
        $children = [];
855
        $nodes = $this->fs->findNodesByFilterUser($deleted, $filter);
856
857
        foreach ($nodes as $node) {
858
            $child = $this->node_decorator->decorate($node, $attributes);
859
            $children[] = $child;
860
        }
861
862
        return (new Response())->setCode(200)->setBody([
863
            'status' => 200,
864
            'data' => $children,
865
        ]);
866
    }
867
868
    /**
869
     * @api {get} /api/v1/node/trash Get trash
870
     * @apiName getTrash
871
     * @apiVersion 1.0.0
872
     * @apiGroup Node
873
     * @apiPermission none
874
     * @apiDescription A similar endpoint to /api/v1/node/query filer={'deleted': {$type: 9}] but instead returning all deleted
875
     * nodes (including children which are deleted as well) this enpoint only returns the first deleted node from every subtree)
876
     * @apiUse _nodeAttributes
877
     *
878
     * @apiExample (cURL) example:
879
     * curl -XGET https://SERVER/api/v1/node/trash?pretty
880
     *
881
     * @apiParam (GET Parameter) {string[]} [attributes] Filter node attributes
882
     *
883
     * @apiSuccess (200 OK) {number} status Status Code
884
     * @apiSuccess (200 OK) {object[]} data Children
885
     * @apiSuccessExample {json} Success-Response:
886
     * HTTP/1.1 200 OK
887
     * {
888
     *      "status":200,
889
     *      "data": [{..}, {...}] //Shorted
890
     * }
891
     */
892
    public function getTrash(array $attributes = []): Response
893
    {
894
        $children = [];
895
        $nodes = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_ONLY, ['deleted' => ['$type' => 9]]);
896
897
        foreach ($nodes as $node) {
898
            try {
899
                $parent = $node->getParent();
900
                if (null !== $parent && $parent->isDeleted()) {
901
                    continue;
902
                }
903
            } catch (\Exception $e) {
904
            }
905
906
            $child = $this->node_decorator->decorate($node, $attributes);
907
            $children[] = $child;
908
        }
909
910
        return (new Response())->setCode(200)->setBody([
911
            'status' => 200,
912
            'data' => array_values($children),
913
        ]);
914
    }
915
916
    /**
917
     * @api {get} /api/v1/node/delta Get delta
918
     * @apiVersion 1.0.0
919
     * @apiName getDelta
920
     * @apiGroup Node
921
     * @apiPermission none
922
     * @apiUse _getNode
923
     *
924
     * @apiDescription Use this method to request a delta feed with all changes on the server (or) a snapshot of the server state.
925
     * 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.
926
     * If has_more is TRUE you need to request /delta immediatly again to
927
     * receive the next bunch of deltas. If has_more is FALSE you should wait at least 120s seconds before any further requests to the
928
     * 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).
929
     * 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
930
     * without a cursor. reset could be TRUE if there was an account maintenance or a simialar case.
931
     * You can request a different limit as well but be aware that the number of nodes could be slighty different from your requested limit.
932
     * If requested with parameter id or p the delta gets generated recursively from the node given.
933
     *
934
     * @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
935
     * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default not all attributes would be returned
936
     * @apiParam (GET Parameter) {string} [cursor=null] Set a cursor to rquest next nodes within delta processing
937
     *
938
     * @apiExample (cURL) example:
939
     * curl -XGET "https://SERVER/api/v1/node/delta?pretty"
940
     *
941
     * @apiSuccess (200 OK) {number} status Status Code
942
     * @apiSuccess (200 OK) {object} data Delta feed
943
     * @apiSuccess (200 OK) {boolean} data.reset If true the local state needs to be reseted, is alway TRUE during
944
     * the first request to /delta without a cursor or in special cases like server or account maintenance
945
     * @apiSuccess (200 OK) {string} data.cursor The cursor needs to be stored and reused to request further deltas
946
     * @apiSuccess (200 OK) {boolean} data.has_more If has_more is TRUE /delta can be requested immediatly after the last request
947
     * to receive further delta. If it is FALSE we should wait at least 120 seconds before any further delta requests to the api endpoint
948
     * @apiSuccess (200 OK) {object[]} data.nodes Node list to process
949
     * @apiSuccess (200 OK) {string} data.nodes.id Node ID
950
     * @apiSuccess (200 OK) {string} data.nodes.deleted Is node deleted?
951
     * @apiSuccess (200 OK) {object} data.nodes.changed Changed timestamp
952
     * @apiSuccess (200 OK) {number} data.nodes.changed.sec Unix timestamp
953
     * @apiSuccess (200 OK) {number} data.nodes.changed.usec Additional Microsecconds to Unix timestamp
954
     * @apiSuccess (200 OK) {object} data.nodes.created Created timestamp (If data.nodes[].deleted is TRUE, created will be NULL)
955
     * @apiSuccess (200 OK) {number} data.nodes.created.sec Unix timestamp
956
     * @apiSuccess (200 OK) {number} data.nodes.created.usec Additional Microsecconds to Unix timestamp
957
     * @apiSuccess (200 OK) {string} data.nodes.path The full absolute path to the node
958
     * @apiSuccess (200 OK) {string} data.nodes.directory Is true if node is a directory
959
     * @apiSuccessExample {json} Success-Response:
960
     * HTTP/1.1 200 OK
961
     * {
962
     *      "status": 200,
963
     *      "data": {
964
     *          "reset": false,
965
     *          "cursor": "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw==",
966
     *          "has_more": false,
967
     *          "nodes": [
968
     *             {
969
     *                  "id": "581afa783c5889ad7c8b4572",
970
     *                  "deleted": true,
971
     *                  "created": null,
972
     *                  "changed": {
973
     *                      "sec": 1478163064,
974
     *                      "usec": 31.0.0
975
     *                  },
976
     *                  "path": "\/AAA\/AL",
977
     *                  "directory": true
978
     *              },
979
     *              {
980
     *                  "id": "581afa783c5889ad7c8b3dcf",
981
     *                  "deleted": false,
982
     *                  "created": {
983
     *                      "sec": 1478163048,
984
     *                      "usec": 101000
985
     *                  },
986
     *                  "changed": {
987
     *                      "sec": 1478163048,
988
     *                      "usec": 101000
989
     *                  },
990
     *                  "path": "\/AL",
991
     *                  "directory": true
992
     *              }
993
     *          ]
994
     *      }
995
     * }
996
     *
997
     * @param string $id
998
     * @param string $p
999
     * @param string $cursor
1000
     */
1001
    public function getDelta(
1002
        DeltaDecorator $delta_decorator,
1003
        ?string $id = null,
1004
        ?string $p = null,
1005
        ?string $cursor = null,
1006
        int $limit = 250,
1007
        array $attributes = []
1008
    ): Response {
1009
        if (null !== $id || null !== $p) {
1010
            $node = $this->_getNode($id, $p);
1011
        } else {
1012
            $node = null;
1013
        }
1014
1015
        $result = $this->fs->getDelta()->getDeltaFeed($cursor, $limit, $node);
1016
1017
        $default = ['id', 'deleted', 'created', 'changed', 'path', 'directory'];
1018
        $attributes = array_merge($default, $attributes);
1019
1020
        foreach ($result['nodes'] as &$node) {
1021
            if ($node instanceof NodeInterface) {
1022
                $node = $this->node_decorator->decorate($node, $attributes);
1023
            } else {
1024
                $node = $delta_decorator->decorate($node, $attributes);
1025
            }
1026
        }
1027
1028
        return (new Response())->setCode(200)->setBody([
1029
            'status' => 200,
1030
            'data' => $result,
1031
        ]);
1032
    }
1033
1034
    /**
1035
     * @api {get} /api/v1/node/event-log?id=:id Event log
1036
     * @apiVersion 1.0.0
1037
     * @apiName getEventLog
1038
     * @apiGroup Node
1039
     * @apiPermission none
1040
     * @apiUse _getNode
1041
     * @apiDescription Get detailed event log
1042
     * Request all modifications which are made by the user himself or share members.
1043
     * Possible operations are the follwing:
1044
     * - deleteCollectionReference
1045
     * - deleteCollectionShare
1046
     * - deleteCollection
1047
     * - addCollection
1048
     * - addFile
1049
     * - addCollectionShare
1050
     * - addCollectionReference
1051
     * - undeleteFile
1052
     * - undeleteCollectionReference
1053
     * - undeleteCollectionShare
1054
     * - restoreFile
1055
     * - renameFile
1056
     * - renameCollection
1057
     * - renameCollectionShare
1058
     * - renameCollectionRFeference
1059
     * - copyFile
1060
     * - copyCollection
1061
     * - copyCollectionShare
1062
     * - copyCollectionRFeference
1063
     * - moveFile
1064
     * - moveCollection
1065
     * - moveCollectionReference
1066
     * - moveCollectionShare
1067
     *
1068
     * @apiExample (cURL) example:
1069
     * curl -XGET "https://SERVER/api/v1/node/event-log?pretty"
1070
     * curl -XGET "https://SERVER/api/v1/node/event-log?id=544627ed3c58891f058b4686&pretty"
1071
     * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/event-log?pretty&limit=10"
1072
     * curl -XGET "https://SERVER/api/v1/node/event-log?p=/absolute/path/to/my/node&pretty"
1073
     *
1074
     * @apiParam (GET Parameter) {number} [limit=100] Sets limit of events to be returned
1075
     * @apiParam (GET Parameter) {number} [skip=0] How many events are skiped (useful for paging)
1076
     *
1077
     * @apiSuccess (200 OK) {number} status Status Code
1078
     * @apiSuccess (200 OK) {object[]} data Events
1079
     * @apiSuccess (200 OK) {number} data.event Event ID
1080
     * @apiSuccess (200 OK) {object} data.timestamp event timestamp
1081
     * @apiSuccess (200 OK) {number} data.timestamp.sec Event timestamp timestamp in Unix time
1082
     * @apiSuccess (200 OK) {number} data.timestamp.usec Additional microseconds to changed Unix timestamp
1083
     * @apiSuccess (200 OK) {string} data.operation event operation (like addCollection, deleteFile, ...)
1084
     * @apiSuccess (200 OK) {string} data.parent ID of the parent node at the time of the event
1085
     * @apiSuccess (200 OK) {object} data.previous Previous state of actual data which has been modified during an event, can contain either version, name or parent
1086
     * @apiSuccess (200 OK) {number} data.previous.version Version at the time before the event
1087
     * @apiSuccess (200 OK) {string} data.previous.name Name at the time before the event
1088
     * @apiSuccess (200 OK) {string} data.previous.parent Parent node at the time before the event
1089
     * @apiSuccess (200 OK) {string} data.share If of the shared folder at the time of the event
1090
     * @apiSuccess (200 OK) {string} data.name Name of the node at the time of the event
1091
     * @apiSuccess (200 OK) {object} data.node Current data of the node (Not from the time of the event!)
1092
     * @apiSuccess (200 OK) {boolean} data.node.deleted True if the node is deleted, false otherwise
1093
     * @apiSuccess (200 OK) {string} data.node.id Actual ID of the node
1094
     * @apiSuccess (200 OK) {string} data.node.name Current name of the node
1095
     * @apiSuccess (200 OK) {object} data.user Data which contains information about the user who executed an event
1096
     * @apiSuccess (200 OK) {string} data.user.id Actual user ID
1097
     * @apiSuccess (200 OK) {string} data.user.username Current username of executed event
1098
     *
1099
     * @apiSuccessExample {json} Success-Response:
1100
     * HTTP/1.1 200 OK
1101
     * {
1102
     *      "status": 200,
1103
     *      "data": [
1104
     *          {
1105
     *              "event": "57628e523c5889026f8b4570",
1106
     *              "timestamp": {
1107
     *                  "sec": 1466076753,
1108
     *                  "usec": 988000
1109
     *              },
1110
     *              "operation": "restoreFile",
1111
     *              "name": "file.txt",
1112
     *              "previous": {
1113
     *                  "version": 16
1114
     *              },
1115
     *              "node": {
1116
     *                  "id": "558c0b273c588963078b457a",
1117
     *                  "name": "3dddsceheckfile.txt",
1118
     *                  "deleted": false
1119
     *              },
1120
     *              "parent": null,
1121
     *              "user": {
1122
     *                  "id": "54354cb63c58891f058b457f",
1123
     *                  "username": "gradmin.bzu"
1124
     *              },
1125
     *              "share": null
1126
     *          }
1127
     *      ]
1128
     * }
1129
     *
1130
     * @param string $id
1131
     * @param string $p
1132
     */
1133
    public function getEventLog(EventDecorator $event_decorator, ?string $id = null, ?string $p = null, int $skip = 0, int $limit = 100): Response
1134
    {
1135
        if (null !== $id || null !== $p) {
1136
            $node = $this->_getNode($id, $p);
1137
        } else {
1138
            $node = null;
1139
        }
1140
1141
        $result = $this->fs->getDelta()->getEventLog($limit, $skip, $node);
1142
        $body = [];
1143
        foreach ($result as $event) {
1144
            $body[] = $event_decorator->decorate($event);
1145
        }
1146
1147
        return (new Response())->setCode(200)->setBody([
1148
            'status' => 200,
1149
            'data' => $body,
1150
        ]);
1151
    }
1152
1153
    /**
1154
     * @api {get} /api/v1/node/last-cursor Get last Cursor
1155
     * @apiVersion 1.0.0
1156
     * @apiName geLastCursor
1157
     * @apiGroup Node
1158
     * @apiUse _getNode
1159
     * @apiPermission none
1160
     * @apiDescription Use this method to request the latest cursor if you only need to now
1161
     * if there are changes on the server. This method will not return any other data than the
1162
     * newest cursor. To request a feed with all deltas request /delta.
1163
     *
1164
     * @apiExample (cURL) example:
1165
     * curl -XGET "https://SERVER/api/v1/node/last-cursor?pretty"
1166
     *
1167
     * @apiSuccess (200 OK) {number} status Status Code
1168
     * @apiSuccess (200 OK) {string} data Newest cursor
1169
     * @apiSuccessExample {json} Success-Response:
1170
     * HTTP/1.1 200 OK
1171
     * {
1172
     *      "status": 200,
1173
     *      "data": "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw=="
1174
     * }
1175
     *
1176
     * @param string $id
1177
     * @param string $p
1178
     */
1179
    public function getLastCursor(?string $id = null, ?string $p = null): Response
1180
    {
1181
        if (null !== $id || null !== $p) {
1182
            $node = $this->_getNode($id, $p);
1183
        } else {
1184
            $node = null;
1185
        }
1186
1187
        $result = $this->fs->getDelta()->getLastCursor();
1188
1189
        return (new Response())->setCode(200)->setBody([
1190
            'status' => 200,
1191
            'data' => $result,
1192
        ]);
1193
    }
1194
1195
    /**
1196
     * Do bulk operations.
1197
     *
1198
     * @param array|string $id
1199
     * @param array|string $p
1200
     */
1201
    protected function bulk($id, $p, Closure $action): Response
1202
    {
1203
        if (is_array($id) || is_array($p)) {
1204
            $errors = [];
1205
            $body = [];
1206
1207
            foreach ($this->_getNodes($id, $p) as $node) {
1208
                try {
1209
                    $body[] = $action->call($this, $node);
1210
                } catch (\Exception $e) {
1211
                    $errors[] = [
1212
                        'error' => get_class($e),
1213
                        'message' => $e->getMessage(),
1214
                        'status' => $e->getCode(),
1215
                    ];
1216
                }
1217
            }
1218
1219
            if (!empty($errors)) {
1220
                return (new Response())->setCode(400)->setBody([
1221
                    'status' => 400,
1222
                    'data' => $errors,
1223
                ]);
1224
            }
1225
            if (empty($body)) {
1226
                return (new Response())->setCode(204);
1227
            }
1228
            $body = array_shift($body);
1229
            $response = (new Response())->setCode($body['status']);
1230
1231
            if (isset($body['data'])) {
1232
                $response->setBody([
1233
                    'status' => $body['status'],
1234
                    'data' => $body['data'],
1235
                ]);
1236
            }
1237
1238
            return $response;
1239
        }
1240
1241
        $body = $action->call($this, $this->_getNode($id, $p));
1242
        $response = (new Response())->setCode($body['status']);
1243
1244
        if (isset($body['data'])) {
1245
            $response->setBody([
1246
                'status' => $body['status'],
1247
                'data' => $body['data'],
1248
            ]);
1249
        }
1250
1251
        return $response;
1252
    }
1253
1254
    /**
1255
     * Get node.
1256
     *
1257
     * @param string $id
1258
     * @param string $path
1259
     * @param string $class      Force set node type
1260
     * @param bool   $multiple   Allow $id to be an array
1261
     * @param bool   $allow_root Allow instance of root collection
1262
     * @param bool   $deleted    How to handle deleted node
1263
     */
1264
    protected function _getNode(
1265
        ?string $id = null,
1266
        ?string $path = null,
1267
        ?string $class = null,
1268
        bool $multiple = false,
1269
        bool $allow_root = false,
1270
        int $deleted = 2
1271
    ): NodeInterface {
1272
        if (null === $class) {
1273
            switch (get_class($this)) {
1274
                case ApiFile::class:
1275
                    $class = File::class;
1276
1277
                break;
1278
                case ApiCollection::class:
1279
                    $class = Collection::class;
1280
1281
                break;
1282
            }
1283
        }
1284
1285
        return $this->fs->getNode($id, $path, $class, $multiple, $allow_root, $deleted);
1286
    }
1287
1288
    /**
1289
     * Get nodes.
1290
     *
1291
     * @param string $id
1292
     * @param string $path
1293
     * @param string $class   Force set node type
1294
     * @param bool   $deleted How to handle deleted node
1295
     */
1296
    protected function _getNodes(
1297
        $id = null,
1298
        $path = null,
1299
        ?string $class = null,
1300
        int $deleted = 2
1301
    ): Generator {
1302
        if (null === $class) {
1303
            switch (get_class($this)) {
1304
                case ApiFile::class:
1305
                    $class = File::class;
1306
1307
                break;
1308
                case ApiCollection::class:
1309
                    $class = Collection::class;
1310
1311
                break;
1312
            }
1313
        }
1314
1315
        return $this->fs->getNodes($id, $path, $class, $deleted);
1316
    }
1317
1318
    /**
1319
     * Merge multiple nodes into one zip archive.
1320
     *
1321
     * @param string $id
1322
     * @param string $path
1323
     */
1324
    protected function combine($id = null, $path = null, string $name = 'selected')
1325
    {
1326
        $archive = new ZipStream($name.'.zip');
1327
1328
        foreach ($this->_getNodes($id, $path) as $node) {
1329
            try {
1330
                $node->zip($archive);
1331
            } catch (\Exception $e) {
1332
                $this->logger->debug('failed zip node in multi node request ['.$node->getId().']', [
1333
                   'category' => get_class($this),
1334
                   'exception' => $e,
1335
               ]);
1336
            }
1337
        }
1338
1339
        $archive->finish();
1340
    }
1341
1342
    /**
1343
     * Check custom node attributes which have to be written.
1344
     */
1345
    protected function _verifyAttributes(array $attributes): array
1346
    {
1347
        $valid_attributes = [
1348
            'changed',
1349
            'destroy',
1350
            'created',
1351
            'meta',
1352
            'readonly',
1353
        ];
1354
1355
        if ($this instanceof ApiCollection) {
1356
            $valid_attributes[] = 'filter';
1357
        }
1358
1359
        $check = array_merge(array_flip($valid_attributes), $attributes);
1360
1361
        if ($this instanceof ApiCollection && count($check) > 6) {
1362
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, filter, readonly and/or meta attributes may be overwritten');
1363
        }
1364
        if ($this instanceof ApiFile && count($check) > 5) {
1365
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, readonly and/or meta attributes may be overwritten');
1366
        }
1367
1368
        foreach ($attributes as $attribute => $value) {
1369
            switch ($attribute) {
1370
                case 'filter':
1371
                    $attributes['filter'] = json_encode((array) $attributes['filter']);
1372
1373
                break;
1374
                case 'destroy':
1375
                    if (!Helper::isValidTimestamp($value)) {
1376
                        throw new Exception\InvalidArgument($attribute.' Changed timestamp must be valid unix timestamp');
1377
                    }
1378
                    $attributes[$attribute] = new UTCDateTime($value.'000');
1379
1380
                break;
1381
                case 'changed':
1382
                case 'created':
1383
                    if (!Helper::isValidTimestamp($value)) {
1384
                        throw new Exception\InvalidArgument($attribute.' Changed timestamp must be valid unix timestamp');
1385
                    }
1386
                    if ((int) $value > time()) {
1387
                        throw new Exception\InvalidArgument($attribute.' timestamp can not be set greater than the server time');
1388
                    }
1389
                    $attributes[$attribute] = new UTCDateTime($value.'000');
1390
1391
                break;
1392
                case 'readonly':
1393
                    $attributes['readonly'] = (bool) $attributes['readonly'];
1394
1395
                break;
1396
            }
1397
        }
1398
1399
        return $attributes;
1400
    }
1401
}
1402