Completed
Push — master ( b97427...e235cc )
by Raffael
30:35 queued 26:08
created

Nodes::get()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 0
cts 25
cp 0
rs 9.456
c 0
b 0
f 0
cc 4
nc 4
nop 6
crap 20
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Api\v2;
13
14
use Balloon\App\Api\Helper as ApiHelper;
15
use Balloon\App\Api\v2\Collections as ApiCollection;
16
use Balloon\App\Api\v2\Files as ApiFile;
17
use Balloon\AttributeDecorator\Pager;
18
use Balloon\Filesystem;
19
use Balloon\Filesystem\DeltaAttributeDecorator;
20
use Balloon\Filesystem\EventAttributeDecorator;
21
use Balloon\Filesystem\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v2\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
22
use Balloon\Filesystem\Node\AttributeDecorator as NodeAttributeDecorator;
23
use Balloon\Filesystem\Node\Collection;
24
use Balloon\Filesystem\Node\NodeInterface;
25
use Balloon\Helper;
26
use Balloon\Server;
27
use Balloon\Server\User;
28
use Micro\Http\Response;
29
use function MongoDB\BSON\fromJSON;
30
use function MongoDB\BSON\toPHP;
31
use MongoDB\BSON\UTCDateTime;
32
use Psr\Log\LoggerInterface;
33
use ZipStream\ZipStream;
34
35
class Nodes extends Controller
36
{
37
    /**
38
     * Filesystem.
39
     *
40
     * @var Filesystem
41
     */
42
    protected $fs;
43
44
    /**
45
     * LoggerInterface.
46
     *
47
     * @var LoggerInterface
48
     */
49
    protected $logger;
50
51
    /**
52
     * Server.
53
     *
54
     * @var Server
55
     */
56
    protected $server;
57
58
    /**
59
     * User.
60
     *
61
     * @var User
62
     */
63
    protected $user;
64
65
    /**
66
     * Node attribute decorator.
67
     *
68
     * @var NodeAttributeDecorator
69
     */
70
    protected $node_decorator;
71
72
    /**
73
     * Initialize.
74
     */
75
    public function __construct(Server $server, NodeAttributeDecorator $decorator, LoggerInterface $logger)
76
    {
77
        $this->fs = $server->getFilesystem();
78
        $this->user = $server->getIdentity();
79
        $this->server = $server;
80
        $this->node_decorator = $decorator;
81
        $this->logger = $logger;
82
    }
83
84
    /**
85
     * Restore node.
86
     */
87
    public function postUndelete(
88
        $id,
89
        bool $move = false,
90
        ?string $destid = null,
91
        int $conflict = 0
92
    ): Response {
93
        $parent = null;
94
        if (true === $move) {
95
            try {
96
                $parent = $this->fs->getNode($destid, Collection::class, false, true);
97
            } catch (Exception\NotFound $e) {
98
                throw new Exception\NotFound(
99
                    'destination collection was not found or is not a collection',
100
                    Exception\NotFound::DESTINATION_NOT_FOUND
101
                );
102
            }
103
        }
104
105
        return $this->bulk($id, function ($node) use ($parent, $conflict, $move) {
106
            if (true === $move) {
107
                $node = $node->setParent($parent, $conflict);
108
            }
109
110
            $node->undelete($conflict);
111
112
            return [
113
                'code' => 200,
114
                'data' => $this->node_decorator->decorate($node),
115
            ];
116
        });
117
    }
118
119
    /**
120
     * Download stream.
121
     *
122
     * @param null|mixed $id
123
     */
124
    public function getContent(
125
        $id = null,
126
        bool $download = false,
127
        string $name = 'selected',
128
        ?string $encoding = null
0 ignored issues
show
Unused Code introduced by
The parameter $encoding is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
129
    ): ?Response {
130
        if (is_array($id)) {
131
            return $this->combine($id, $name);
132
        }
133
134
        $node = $this->_getNode($id);
135
        if ($node instanceof Collection) {
136
            return $node->getZip();
137
        }
138
139
        $response = new Response();
140
141
        return ApiHelper::streamContent($response, $node, $download);
142
    }
143
144
    /**
145
     * Get attributes.
146
     *
147
     * @param null|mixed $id
148
     * @param null|mixed $query
149
     */
150
    public function get($id = null, int $deleted = 0, $query = null, array $attributes = [], int $offset = 0, int $limit = 20): Response
151
    {
152
        if ($id === null) {
153
            $query = $this->parseQuery($query);
154
155
            if ($this instanceof ApiFile) {
156
                $query['directory'] = false;
157
                $uri = '/api/v2/files';
158
            } elseif ($this instanceof ApiCollection) {
159
                $query['directory'] = true;
160
                $uri = '/api/v2/collections';
161
            } else {
162
                $uri = '/api/v2/nodes';
163
            }
164
165
            $nodes = $this->fs->findNodesByFilterUser($deleted, $query, $offset, $limit);
166
            $pager = new Pager($this->node_decorator, $nodes, $attributes, $offset, $limit, $uri);
167
            $result = $pager->paging();
168
169
            return (new Response())->setCode(200)->setBody($result);
170
        }
171
172
        return $this->bulk($id, function ($node) use ($attributes) {
173
            return [
174
                'code' => 200,
175
                'data' => $this->node_decorator->decorate($node, $attributes),
176
            ];
177
        });
178
    }
179
180
    /**
181
     * Get parent nodes.
182
     */
183
    public function getParents(string $id, array $attributes = [], bool $self = false): Response
184
    {
185
        $result = [];
186
        $request = $this->_getNode($id);
187
        $parents = $request->getParents();
188
189
        if (true === $self && $request instanceof Collection) {
190
            $result[] = $this->node_decorator->decorate($request, $attributes);
191
        }
192
193
        foreach ($parents as $node) {
194
            $result[] = $this->node_decorator->decorate($node, $attributes);
195
        }
196
197
        return (new Response())->setCode(200)->setBody($result);
198
    }
199
200
    /**
201
     * Change attributes.
202
     *
203
     * @param null|mixed $lock
204
     */
205
    public function patch(string $id, ?string $name = null, ?array $meta = null, ?bool $readonly = null, ?array $filter = null, ?array $acl = null, $lock = null): Response
206
    {
207
        $attributes = compact('name', 'meta', 'readonly', 'filter', 'acl', 'lock');
208
        $attributes = array_filter($attributes, function ($attribute) {return !is_null($attribute); });
209
210
        $lock = $_SERVER['HTTP_LOCK_TOKEN'] ?? null;
211
212
        return $this->bulk($id, function ($node) use ($attributes, $lock) {
213
            foreach ($attributes as $attribute => $value) {
214
                switch ($attribute) {
215
                    case 'name':
216
                        $node->setName($value);
217
218
                    break;
219
                    case 'meta':
220
                        $node->setMetaAttributes($value);
221
222
                    break;
223
                    case 'readonly':
224
                        $node->setReadonly($value);
225
226
                    break;
227
                    case 'filter':
228
                        if ($node instanceof Collection) {
229
                            $node->setFilter($value);
230
                        }
231
232
                    break;
233
                    case 'acl':
234
                        $node->setAcl($value);
235
236
                    break;
237
                    case 'lock':
238
                        if ($value === false) {
239
                            $node->unlock($lock);
240
                        } else {
241
                            $node->lock($lock);
242
                        }
243
244
                    break;
245
                }
246
            }
247
248
            return [
249
                'code' => 200,
250
                'data' => $this->node_decorator->decorate($node),
251
            ];
252
        });
253
    }
254
255
    /**
256
     * Clone node.
257
     */
258
    public function postClone(
259
        $id,
260
        ?string $destid = null,
261
        int $conflict = 0
262
    ): Response {
263
        try {
264
            $parent = $this->fs->getNode($destid, Collection::class, false, true);
265
        } catch (Exception\NotFound $e) {
266
            throw new Exception\NotFound(
267
                'destination collection was not found or is not a collection',
268
                Exception\NotFound::DESTINATION_NOT_FOUND
269
            );
270
        }
271
272
        return $this->bulk($id, function ($node) use ($parent, $conflict) {
273
            $result = $node->copyTo($parent, $conflict);
274
275
            return [
276
                'code' => $parent == $result ? 200 : 201,
277
                'data' => $this->node_decorator->decorate($result),
278
            ];
279
        });
280
    }
281
282
    /**
283
     * Move node.
284
     */
285
    public function postMove(
286
        $id,
287
        ?string $destid = null,
288
        int $conflict = 0
289
    ): Response {
290
        try {
291
            $parent = $this->fs->getNode($destid, Collection::class, false, true);
292
        } catch (Exception\NotFound $e) {
293
            throw new Exception\NotFound(
294
                'destination collection was not found or is not a collection',
295
                Exception\NotFound::DESTINATION_NOT_FOUND
296
            );
297
        }
298
299
        return $this->bulk($id, function ($node) use ($parent, $conflict) {
300
            $result = $node->setParent($parent, $conflict);
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
301
302
            return [
303
                'code' => 200,
304
                'data' => $this->node_decorator->decorate($node),
305
            ];
306
        });
307
    }
308
309
    /**
310
     * Delete node.
311
     */
312
    public function delete(
313
        $id,
314
        bool $force = false,
315
        bool $ignore_flag = false,
316
        ?string $at = null
317
    ): Response {
318
        $failures = [];
0 ignored issues
show
Unused Code introduced by
$failures is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
319
320
        if (null !== $at && '0' !== $at) {
321
            $at = $this->_verifyAttributes(['destroy' => $at])['destroy'];
322
        }
323
324
        return $this->bulk($id, function ($node) use ($force, $ignore_flag, $at) {
325
            if (null === $at) {
326
                $node->delete($force && $node->isDeleted() || $force && $ignore_flag);
327
            } else {
328
                if ('0' === $at) {
329
                    $at = null;
0 ignored issues
show
Bug introduced by
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...
330
                }
331
                $node->setDestroyable($at);
332
            }
333
334
            return [
335
                'code' => 204,
336
            ];
337
        });
338
    }
339
340
    /**
341
     * Get trash.
342
     *
343
     * @param null|mixed $query
344
     */
345
    public function getTrash($query = null, array $attributes = [], int $offset = 0, int $limit = 20): Response
346
    {
347
        $children = [];
348
        $query = $this->parseQuery($query);
349
        $filter = ['deleted' => ['$type' => 9]];
350
351
        if (!empty($query)) {
352
            $filter = [
353
                $filter,
354
                $query,
355
            ];
356
        }
357
358
        $nodes = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_ONLY, $filter, $offset, $limit);
359
360
        foreach ($nodes as $node) {
361
            try {
362
                $parent = $node->getParent();
363
                if (null !== $parent && $parent->isDeleted()) {
364
                    continue;
365
                }
366
            } catch (\Exception $e) {
367
                //skip exception
368
            }
369
370
            $children[] = $node;
371
        }
372
373
        if ($this instanceof ApiFile) {
374
            $query['directory'] = false;
375
            $uri = '/api/v2/files';
376
        } elseif ($this instanceof ApiCollection) {
377
            $query['directory'] = true;
378
            $uri = '/api/v2/collections';
379
        } else {
380
            $uri = '/api/v2/nodes';
381
        }
382
383
        $pager = new Pager($this->node_decorator, $children, $attributes, $offset, $limit, $uri, $nodes->getReturn());
384
        $result = $pager->paging();
385
386
        return (new Response())->setCode(200)->setBody($result);
387
    }
388
389
    /**
390
     * Get delta.
391
     */
392
    public function getDelta(
393
        DeltaAttributeDecorator $delta_decorator,
394
        ?string $id = null,
395
        ?string $cursor = null,
396
        int $limit = 250,
397
        array $attributes = []
398
    ): Response {
399
        if (null !== $id) {
400
            $node = $this->_getNode($id);
401
        } else {
402
            $node = null;
403
        }
404
405
        $result = $this->fs->getDelta()->getDeltaFeed($cursor, $limit, $node);
406
        foreach ($result['nodes'] as &$node) {
407
            if ($node instanceof NodeInterface) {
408
                $node = $this->node_decorator->decorate($node, $attributes);
409
            } else {
410
                $node = $delta_decorator->decorate($node, $attributes);
411
            }
412
        }
413
414
        return (new Response())->setCode(200)->setBody($result);
415
    }
416
417
    /**
418
     * Event log.
419
     *
420
     * @param null|mixed $query
421
     */
422
    public function getEventLog(EventAttributeDecorator $event_decorator, ?string $id = null, $query = null, ?string $sort = null, ?array $attributes = [], int $offset = 0, int $limit = 20): Response
0 ignored issues
show
Unused Code introduced by
The parameter $sort is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
423
    {
424
        if (null !== $id) {
425
            $node = $this->_getNode($id);
426
            $uri = '/api/v2/nodes/'.$node->getId().'/event-log';
427
        } else {
428
            $node = null;
429
            $uri = '/api/v2/nodes/event-log';
430
        }
431
432
        $query = $this->parseQuery($query);
433
        $result = $this->fs->getDelta()->getEventLog($query, $limit, $offset, $node, $total);
434
        $pager = new Pager($event_decorator, $result, $attributes, $offset, $limit, $uri, $total);
435
436
        return (new Response())->setCode(200)->setBody($pager->paging());
437
    }
438
439
    /**
440
     * Get last Cursor.
441
     */
442
    public function getLastCursor(?string $id = null): Response
443
    {
444
        if (null !== $id) {
445
            $node = $this->_getNode($id);
0 ignored issues
show
Unused Code introduced by
$node is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
446
        } else {
447
            $node = null;
0 ignored issues
show
Unused Code introduced by
$node is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
448
        }
449
450
        $result = $this->fs->getDelta()->getLastCursor();
451
452
        return (new Response())->setCode(200)->setBody($result);
453
    }
454
455
    /**
456
     * Parse query.
457
     */
458
    protected function parseQuery($query): array
459
    {
460
        if ($query === null) {
461
            $query = [];
462
        } elseif (is_string($query)) {
463
            $query = toPHP(fromJSON($query), [
464
                'root' => 'array',
465
                'document' => 'array',
466
                'array' => 'array',
467
            ]);
468
        }
469
470
        return (array) $query;
471
    }
472
473
    /**
474
     * Merge multiple nodes into one zip archive.
475
     *
476
     * @param null|mixed $id
477
     */
478
    protected function combine($id = null, string $name = 'selected')
479
    {
480
        $archive = new ZipStream($name.'.zip');
481
482
        foreach ($this->_getNodes($id) as $node) {
483
            try {
484
                $node->zip($archive);
485
            } catch (\Exception $e) {
486
                $this->logger->debug('failed zip node in multi node request ['.$node->getId().']', [
487
                   'category' => get_class($this),
488
                   'exception' => $e,
489
               ]);
490
            }
491
        }
492
493
        $archive->finish();
494
    }
495
496
    /**
497
     * Check custom node attributes which have to be written.
498
     */
499
    protected function _verifyAttributes(array $attributes): array
500
    {
501
        $valid_attributes = [
502
            'changed',
503
            'destroy',
504
            'created',
505
            'meta',
506
            'readonly',
507
            'acl',
508
            'lock',
509
        ];
510
511
        if ($this instanceof ApiCollection) {
512
            $valid_attributes += ['filter', 'mount'];
513
        }
514
515
        $check = array_merge(array_flip($valid_attributes), $attributes);
516
517
        if ($this instanceof ApiCollection && count($check) > 9) {
518
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, lock, filter, mount, readonly and/or meta attributes may be overwritten');
519
        }
520
        if ($this instanceof ApiFile && count($check) > 7) {
521
            throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, acl, lock, readonly and/or meta attributes may be overwritten');
522
        }
523
524
        foreach ($attributes as $attribute => $value) {
525
            switch ($attribute) {
526
                case 'filter':
527
                    if (!is_array($value)) {
528
                        throw new Exception\InvalidArgument($attribute.' must be an array');
529
                    }
530
531
                    $attributes['filter'] = json_encode($value);
532
533
                break;
534
                case 'destroy':
535
                    if (!Helper::isValidTimestamp($value)) {
536
                        throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp');
537
                    }
538
                    $attributes[$attribute] = new UTCDateTime($value.'000');
539
540
                break;
541
                case 'changed':
542
                case 'created':
543
                    if (!Helper::isValidTimestamp($value)) {
544
                        throw new Exception\InvalidArgument($attribute.' timestamp must be valid unix timestamp');
545
                    }
546
                    if ((int) $value > time()) {
547
                        throw new Exception\InvalidArgument($attribute.' timestamp can not be set greater than the server time');
548
                    }
549
                    $attributes[$attribute] = new UTCDateTime($value.'000');
550
551
                break;
552
                case 'readonly':
553
                    $attributes['readonly'] = (bool) $attributes['readonly'];
554
555
                break;
556
            }
557
        }
558
559
        return $attributes;
560
    }
561
}
562