Test Failed
Pull Request — master (#350)
by Raffael
26:39
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);
0 ignored issues
show
Compatibility introduced by
$node of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
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