Completed
Push — master ( afaf42...8ac772 )
by Raffael
20:10 queued 16:21
created

Filesystem::initNode()   C

Complexity

Conditions 16
Paths 54

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 272

Importance

Changes 0
Metric Value
dl 0
loc 55
ccs 0
cts 29
cp 0
rs 5.5666
c 0
b 0
f 0
cc 16
nc 54
nop 1
crap 272

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon;
13
14
use Balloon\Filesystem\Acl;
15
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
16
use Balloon\Filesystem\Delta;
17
use Balloon\Filesystem\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\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...
18
use Balloon\Filesystem\Node\Collection;
19
use Balloon\Filesystem\Node\Factory as NodeFactory;
20
use Balloon\Filesystem\Node\NodeInterface;
21
use Balloon\Server\User;
22
use Generator;
23
use MongoDB\BSON\ObjectId;
24
use MongoDB\BSON\UTCDateTime;
25
use MongoDB\Database;
26
use Psr\Log\LoggerInterface;
27
28
class Filesystem
29
{
30
    /**
31
     * Database.
32
     *
33
     * @var Database
34
     */
35
    protected $db;
36
37
    /**
38
     * LoggerInterface.
39
     *
40
     * @var LoggerInterface
41
     */
42
    protected $logger;
43
44
    /**
45
     * Hook.
46
     *
47
     * @var Hook
48
     */
49
    protected $hook;
50
51
    /**
52
     * Server.
53
     *
54
     * @var Server
55
     */
56
    protected $server;
57
58
    /**
59
     * Root collection.
60
     *
61
     * @var Collection
62
     */
63
    protected $root;
64
65
    /**
66
     * User.
67
     *
68
     * @var Delta
69
     */
70
    protected $delta;
71
72
    /**
73
     * Get user.
74
     *
75
     * @var User
76
     */
77
    protected $user;
78
79
    /**
80
     * Node factory.
81
     *
82
     * @var NodeFactory
83
     */
84
    protected $node_factory;
85
86
    /**
87
     * Acl.
88
     *
89
     * @var Acl
90
     */
91
    protected $acl;
92
93
    /**
94
     * Node storage cache.
95
     *
96
     * @var array
97
     */
98
    protected $cache = [];
99
100
    /**
101
     * Node storage cache.
102
     *
103
     * @var array
104
     */
105
    protected $raw_cache = [];
106
107
    /**
108
     * Initialize.
109
     *
110
     * @param User $user
111
     */
112
    public function __construct(Server $server, Database $db, Hook $hook, LoggerInterface $logger, NodeFactory $node_factory, Acl $acl, ?User $user = null)
113
    {
114
        $this->user = $user;
115
        $this->server = $server;
116
        $this->db = $db;
117
        $this->logger = $logger;
118
        $this->hook = $hook;
119
        $this->node_factory = $node_factory;
120
        $this->acl = $acl;
121
    }
122
123
    /**
124
     * Get user.
125
     *
126
     * @return User
127
     */
128
    public function getUser(): ?User
129
    {
130
        return $this->user;
131
    }
132
133
    /**
134
     * Get server.
135
     */
136
    public function getServer(): Server
137
    {
138
        return $this->server;
139
    }
140
141
    /**
142
     * Get database.
143
     */
144
    public function getDatabase(): Database
145
    {
146
        return $this->db;
147
    }
148
149
    /**
150
     * Get root.
151
     */
152
    public function getRoot(): Collection
153
    {
154
        if ($this->root instanceof Collection) {
155
            return $this->root;
156
        }
157
158
        return $this->root = $this->initNode([
159
            'directory' => true,
160
            '_id' => null,
161
            'owner' => $this->user ? $this->user->getId() : null,
162
        ]);
163
    }
164
165
    /**
166
     * Get delta.
167
     */
168
    public function getDelta(): Delta
169
    {
170
        if ($this->delta instanceof Delta) {
171
            return $this->delta;
172
        }
173
174
        return $this->delta = new Delta($this, $this->db);
175
    }
176
177
    /**
178
     * Find raw node.
179
     */
180
    public function findRawNode(ObjectId $id): array
181
    {
182
        if (isset($this->raw_cache[(string) $id])) {
183
            return $this->raw_cache[(string) $id];
184
        }
185
186
        $node = $this->db->storage->findOne(['_id' => $id]);
187
        if (null === $node) {
188
            throw new Exception\NotFound(
189
                'node '.$id.' not found',
190
                Exception\NotFound::NODE_NOT_FOUND
191
            );
192
        }
193
194
        $this->raw_cache[(string) $id] = $node;
195
196
        return $node;
197
    }
198
199
    /**
200
     * Factory loader.
201
     *
202
     * @param ObjectId|string $id
203
     * @param string          $class Fore check node type
204
     */
205
    public function findNodeById($id, ?string $class = null, int $deleted = NodeInterface::DELETED_INCLUDE): NodeInterface
206
    {
207
        if (isset($this->cache[(string) $id])) {
208
            return $this->cache[(string) $id];
209
        }
210
211
        if (!is_string($id) && !($id instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
212
            throw new Exception\InvalidArgument($id.' node id has to be a string or instance of \MongoDB\BSON\ObjectId');
213
        }
214
215
        try {
216
            if (is_string($id)) {
217
                $id = new ObjectId($id);
218
            }
219
        } catch (\Exception $e) {
220
            throw new Exception\InvalidArgument('invalid node id specified');
221
        }
222
223
        $filter = [
224
            '_id' => $id,
225
        ];
226
227
        switch ($deleted) {
228
            case NodeInterface::DELETED_INCLUDE:
229
                break;
230
            case NodeInterface::DELETED_EXCLUDE:
231
                $filter['deleted'] = false;
232
233
                break;
234
            case NodeInterface::DELETED_ONLY:
235
                $filter['deleted'] = ['$type' => 9];
236
237
                break;
238
        }
239
240
        $node = $this->db->storage->findOne($filter);
241
242
        if (null === $node) {
243
            throw new Exception\NotFound(
244
                'node ['.$id.'] not found',
245
                Exception\NotFound::NODE_NOT_FOUND
246
            );
247
        }
248
249
        $return = $this->initNode($node);
250
251
        if (null !== $class && !($return instanceof $class)) {
252
            throw new Exception('node '.get_class($return).' is not instance of '.$class);
253
        }
254
255
        return $return;
256
    }
257
258
    /**
259
     * Load node with path.
260
     *
261
     * @param string $class Fore check node type
262
     */
263
    public function findNodeByPath(string $path = '', ?string $class = null): NodeInterface
264
    {
265
        if (empty($path) || '/' !== $path[0]) {
266
            $path = '/'.$path;
267
        }
268
269
        $last = strlen($path) - 1;
270
        if ('/' === $path[$last]) {
271
            $path = substr($path, 0, -1);
272
        }
273
274
        $parts = explode('/', $path);
275
        $parent = $this->getRoot();
276
        array_shift($parts);
277
        foreach ($parts as $node) {
278
            $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE);
279
        }
280
281
        if (null !== $class && !($parent instanceof $class)) {
282
            throw new Exception('node is not instance of '.$class);
283
        }
284
285
        return $parent;
286
    }
287
288
    /**
289
     * Load nodes by id.
290
     *
291
     * @param string $class   Force check node type
292
     * @param bool   $deleted
293
     */
294
    public function findNodesById(array $id = [], ?string $class = null, int $deleted = NodeInterface::DELETED_INCLUDE): Generator
295
    {
296
        $find = [];
297
        foreach ($id as $i) {
298
            $find[] = new ObjectId($i);
299
        }
300
301
        $filter = [
302
            '_id' => ['$in' => $find],
303
        ];
304
305
        switch ($deleted) {
306
            case NodeInterface::DELETED_INCLUDE:
307
                break;
308
            case NodeInterface::DELETED_EXCLUDE:
309
                $filter['deleted'] = false;
310
311
                break;
312
            case NodeInterface::DELETED_ONLY:
313
                $filter['deleted'] = ['$type' => 9];
314
315
                break;
316
        }
317
318
        $result = $this->db->storage->find($filter);
319
320
        $nodes = [];
321
        foreach ($result as $node) {
322
            try {
323
                $return = $this->initNode($node);
324
325
                if (in_array($return->getId(), $nodes)) {
326
                    continue;
327
                }
328
329
                $nodes[] = $return->getId();
330
            } catch (\Exception $e) {
331
                $this->logger->error('remove node from result list, failed load node', [
332
                    'category' => get_class($this),
333
                    'exception' => $e,
334
                ]);
335
336
                continue;
337
            }
338
339
            if (null !== $class && !($return instanceof $class)) {
340
                throw new Exception('node is not an instance of '.$class);
341
            }
342
343
            yield $return;
344
        }
345
    }
346
347
    /**
348
     * Load nodes by id.
349
     *
350
     * @param string $class Force check node type
351
     */
352
    public function findNodesByPath(array $path = [], ?string $class = null): Generator
353
    {
354
        $find = [];
0 ignored issues
show
Unused Code introduced by
$find 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...
355
        foreach ($path as $p) {
0 ignored issues
show
Bug introduced by
The expression $path of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
356
            if (empty($path) || '/' !== $path[0]) {
357
                $path = '/'.$path;
358
            }
359
360
            $last = strlen($path) - 1;
361
            if ('/' === $path[$last]) {
362
                $path = substr($path, 0, -1);
363
            }
364
365
            $parts = explode('/', $path);
366
            $parent = $this->getRoot();
367
            array_shift($parts);
368
            foreach ($parts as $node) {
369
                $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE);
370
            }
371
372
            if (null !== $class && !($parent instanceof $class)) {
373
                throw new Exception('node is not an instance of '.$class);
374
            }
375
376
            yield $parent;
377
        }
378
    }
379
380
    /**
381
     * Load nodes by id.
382
     *
383
     * @param array  $id
384
     * @param array  $path
385
     * @param string $class Force set node type
386
     */
387
    public function getNodes(?array $id = null, ?array $path = null, $class = null, int $deleted = NodeInterface::DELETED_EXCLUDE): Generator
388
    {
389
        if (null === $id && null === $path) {
390
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
391
        }
392
        if (null !== $id && null !== $path) {
393
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
394
        }
395
        if (null !== $id) {
396
            if (null === $deleted) {
397
                $deleted = NodeInterface::DELETED_INCLUDE;
398
            }
399
400
            return $this->findNodesById($id, $class, $deleted);
401
        }
402
        if (null !== $path) {
403
            if (null === $deleted) {
404
                $deleted = NodeInterface::DELETED_EXCLUDE;
0 ignored issues
show
Unused Code introduced by
$deleted 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...
405
            }
406
407
            return $this->findNodesByPath($path, $class);
408
        }
409
    }
410
411
    /**
412
     * Load node.
413
     *
414
     * @param string $id
415
     * @param string $path
416
     * @param string $class      Force set node type
417
     * @param bool   $multiple   Allow $id to be an array
418
     * @param bool   $allow_root Allow instance of root collection
419
     * @param bool   $deleted    How to handle deleted node
420
     */
421
    public function getNode($id = null, $path = null, $class = null, bool $multiple = false, bool $allow_root = false, ?int $deleted = null): NodeInterface
422
    {
423
        if (empty($id) && empty($path)) {
424
            if (true === $allow_root) {
425
                return $this->getRoot();
426
            }
427
428
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
429
        }
430
        if (null !== $id && null !== $path) {
431
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
432
        }
433
        if (null !== $id) {
434
            if (null === $deleted) {
435
                $deleted = NodeInterface::DELETED_INCLUDE;
436
            }
437
438
            if (true === $multiple && is_array($id)) {
439
                return $this->findNodesById($id, $class, $deleted);
440
            }
441
442
            return $this->findNodeById($id, $class, $deleted);
443
        }
444
        if (null !== $path) {
445
            if (null === $deleted) {
446
                $deleted = NodeInterface::DELETED_EXCLUDE;
0 ignored issues
show
Unused Code introduced by
$deleted 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...
447
            }
448
449
            return $this->findNodeByPath($path, $class);
450
        }
451
    }
452
453
    /**
454
     * Find node with custom filter.
455
     */
456
    public function findNodeByFilter(array $filter): NodeInterface
457
    {
458
        $result = $this->db->storage->findOne($filter);
459
        if (null === $result) {
460
            throw new Exception\NotFound(
461
                'node with custom filter was not found',
462
                Exception\NotFound::NODE_NOT_FOUND
463
            );
464
        }
465
466
        return $this->initNode($result);
467
    }
468
469
    /**
470
     * Find nodes with custom filters.
471
     */
472
    public function findNodesByFilter(array $filter, ?int $offset = null, ?int $limit = null): Generator
473
    {
474
        $result = $this->db->storage->find($filter, [
475
            'skip' => $offset,
476
            'limit' => $limit,
477
        ]);
478
479
        $count = $this->db->storage->count($filter);
480
481
        foreach ($result as $node) {
482
            try {
483
                yield $this->initNode($node);
484
            } catch (\Exception $e) {
485
                $this->logger->error('remove node from result list, failed load node', [
486
                    'category' => get_class($this),
487
                    'exception' => $e,
488
                ]);
489
            }
490
        }
491
492
        return $count;
493
    }
494
495
    /**
496
     * Get custom filtered children.
497
     */
498
    public function findNodesByFilterUser(int $deleted, array $filter, ?int $offset = null, ?int $limit = null): Generator
499
    {
500
        $shares = $this->user->getShares();
501
        $stored_filter = ['$and' => [
502
            [],
503
            ['$or' => [
504
                ['owner' => $this->user->getId()],
505
                ['shared' => ['$in' => $shares]],
506
            ]],
507
        ]];
508
509
        if (NodeInterface::DELETED_EXCLUDE === $deleted) {
510
            $stored_filter['$and'][0]['deleted'] = false;
511
        } elseif (NodeInterface::DELETED_ONLY === $deleted) {
512
            $stored_filter['$and'][0]['deleted'] = ['$type' => 9];
513
        }
514
515
        $stored_filter['$and'][0] = array_merge($filter, $stored_filter['$and'][0]);
516
517
        $result = $this->db->storage->find($stored_filter, [
518
            'skip' => $offset,
519
            'limit' => $limit,
520
        ]);
521
522
        $count = $this->db->storage->count($stored_filter);
523
524
        foreach ($result as $node) {
525
            try {
526
                yield $this->initNode($node);
527
            } catch (\Exception $e) {
528
                $this->logger->error('remove node from result list, failed load node', [
529
                    'category' => get_class($this),
530
                    'exception' => $e,
531
                ]);
532
            }
533
        }
534
535
        return $count;
536
    }
537
538
    /**
539
     * Init node.
540
     */
541
    public function initNode(array $node): NodeInterface
542
    {
543
        $id = $node['_id'];
0 ignored issues
show
Unused Code introduced by
$id 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...
544
545
        if (isset($node['shared']) && true === $node['shared'] && null !== $this->user && $node['owner'] != $this->user->getId()) {
546
            $node = $this->findReferenceNode($node);
547
        }
548
549
        if (isset($node['parent'])) {
550
            $parent = $this->findNodeById($node['parent']);
551
        } elseif ($node['_id'] !== null) {
552
            $parent = $this->getRoot();
553
        } else {
554
            $parent = null;
555
        }
556
557
        if (!array_key_exists('directory', $node)) {
558
            throw new Exception('invalid node ['.$node['_id'].'] found, directory attribute does not exists');
559
        }
560
561
        $instance = $this->node_factory->build($this, $node, $parent);
562
563
        if (!$this->acl->isAllowed($instance, 'r')) {
564
            if ($instance->isReference()) {
565
                $instance->delete(true);
566
            }
567
568
            throw new ForbiddenException(
569
                'not allowed to access node',
570
                ForbiddenException::NOT_ALLOWED_TO_ACCESS
571
            );
572
        }
573
574
        $loaded = isset($this->cache[(string) $node['_id']]);
575
576
        if ($loaded === false) {
577
            $this->cache[(string) $node['_id']] = $instance;
578
        }
579
580
        if ($loaded === false && isset($node['destroy']) && $node['destroy'] instanceof UTCDateTime && $node['destroy']->toDateTime()->format('U') <= time()) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
581
            $this->logger->info('node ['.$node['_id'].'] is not accessible anmyore, destroy node cause of expired destroy flag', [
582
                'category' => get_class($this),
583
            ]);
584
585
            $instance->delete(true);
586
587
            throw new Exception\Conflict('node is not available anymore');
588
        }
589
590
        if (PHP_SAPI === 'cli') {
591
            unset($this->cache[(string) $node['_id']]);
592
        }
593
594
        return $instance;
595
    }
596
597
    /**
598
     * Resolve shared node to reference or share depending who requested.
599
     */
600
    protected function findReferenceNode(array $node): array
601
    {
602
        if (isset($node['reference']) && ($node['reference'] instanceof ObjectId)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
603
            $this->logger->debug('reference node ['.$node['_id'].'] requested from share owner, trying to find the shared node', [
604
                'category' => get_class($this),
605
            ]);
606
607
            $result = $this->db->storage->findOne([
608
                'owner' => $this->user->getId(),
609
                'shared' => true,
610
                '_id' => $node['reference'],
611
            ]);
612
613
            if (null === $result) {
614
                throw new Exception\NotFound(
615
                    'no share node for reference node '.$node['reference'].' found',
616
                    Exception\NotFound::SHARE_NOT_FOUND
617
                );
618
            }
619
        } else {
620
            $this->logger->debug('share node ['.$node['_id'].'] requested from member, trying to find the reference node', [
621
                'category' => get_class($this),
622
            ]);
623
624
            $result = $this->db->storage->findOne([
625
                'owner' => $this->user->getId(),
626
                'shared' => true,
627
                'reference' => $node['_id'],
628
            ]);
629
630
            if (null === $result) {
631
                throw new Exception\NotFound(
632
                    'no share reference for node '.$node['_id'].' found',
633
                    Exception\NotFound::REFERENCE_NOT_FOUND
634
                );
635
            }
636
        }
637
638
        return $result;
639
    }
640
}
641