Completed
Branch dev (d5d70c)
by Raffael
11:00
created

Filesystem::findNodesByFilter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
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\Node\Collection;
18
use Balloon\Filesystem\Node\File;
19
use Balloon\Filesystem\Node\NodeInterface;
20
use Balloon\Filesystem\Storage;
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
     * Storage.
81
     *
82
     * @var Storage
83
     */
84
    protected $storage;
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
     * Initialize.
102
     *
103
     * @param Server          $server
104
     * @param LoggerInterface $logger
105
     * @param User            $user
106
     */
107
    public function __construct(Server $server, Database $db, Hook $hook, LoggerInterface $logger, Storage $storage, Acl $acl, ?User $user = null)
108
    {
109
        $this->user = $user;
110
        $this->server = $server;
111
        $this->db = $db;
112
        $this->logger = $logger;
113
        $this->hook = $hook;
114
        $this->storage = $storage;
115
        $this->acl = $acl;
116
    }
117
118
    /**
119
     * Get user.
120
     *
121
     * @return User
122
     */
123
    public function getUser(): ?User
124
    {
125
        return $this->user;
126
    }
127
128
    /**
129
     * Get server.
130
     *
131
     * @return Server
132
     */
133
    public function getServer(): Server
134
    {
135
        return $this->server;
136
    }
137
138
    /**
139
     * Get database.
140
     *
141
     * @return Database
142
     */
143
    public function getDatabase(): Database
144
    {
145
        return $this->db;
146
    }
147
148
    /**
149
     * Get root.
150
     *
151
     * @return Collection
152
     */
153
    public function getRoot(): Collection
154
    {
155
        if ($this->root instanceof Collection) {
156
            return $this->root;
157
        }
158
159
        return $this->root = $this->initNode([
160
            'directory' => true,
161
            '_id' => null,
162
            'owner' => $this->user ? $this->user->getId() : null,
163
        ]);
164
    }
165
166
    /**
167
     * Get delta.
168
     *
169
     * @return Delta
170
     */
171
    public function getDelta(): Delta
172
    {
173
        if ($this->delta instanceof Delta) {
174
            return $this->delta;
175
        }
176
177
        return $this->delta = new Delta($this, $this->db);
178
    }
179
180
    /**
181
     * Find raw node.
182
     *
183
     * @param ObjectId $id
184
     *
185
     * @return array
186
     */
187
    public function findRawNode(ObjectId $id): array
188
    {
189
        if (isset($this->cache[(string) $id])) {
190
            return $this->cache[(string) $id]->getRawAttributes();
191
        }
192
193
        $node = $this->db->storage->findOne(['_id' => $id]);
194
        if (null === $node) {
195
            throw new Exception\NotFound(
196
                'node '.$id.' not found',
197
                Exception\NotFound::NODE_NOT_FOUND
198
            );
199
        }
200
201
        return $node;
202
    }
203
204
    /**
205
     * Factory loader.
206
     *
207
     * @param ObjectId|string $id
208
     * @param string          $class   Fore check node type
209
     * @param int             $deleted
210
     *
211
     * @return NodeInterface
212
     */
213
    public function findNodeById($id, ?string $class = null, int $deleted = NodeInterface::DELETED_INCLUDE): NodeInterface
214
    {
215
        if (isset($this->cache[(string) $id])) {
216
            return $this->cache[(string) $id];
217
        }
218
219
        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...
220
            throw new Exception\InvalidArgument($id.' node id has to be a string or instance of \MongoDB\BSON\ObjectId');
221
        }
222
223
        try {
224
            if (is_string($id)) {
225
                $id = new ObjectId($id);
226
            }
227
        } catch (\Exception $e) {
228
            throw new Exception\InvalidArgument('invalid node id specified');
229
        }
230
231
        $filter = [
232
            '_id' => $id,
233
        ];
234
235
        switch ($deleted) {
236
            case NodeInterface::DELETED_INCLUDE:
237
                break;
238
            case NodeInterface::DELETED_EXCLUDE:
239
                $filter['deleted'] = false;
240
241
                break;
242
            case NodeInterface::DELETED_ONLY:
243
                $filter['deleted'] = ['$type' => 9];
244
245
                break;
246
        }
247
248
        $node = $this->db->storage->findOne($filter);
249
250
        if (null === $node) {
251
            throw new Exception\NotFound(
252
                'node ['.$id.'] not found',
253
                Exception\NotFound::NODE_NOT_FOUND
254
            );
255
        }
256
257
        $return = $this->initNode($node);
258
259
        if (null !== $class && !($return instanceof $class)) {
260
            throw new Exception('node '.get_class($return).' is not instance of '.$class);
261
        }
262
263
        return $return;
264
    }
265
266
    /**
267
     * Load node with path.
268
     *
269
     * @param string $path
270
     * @param string $class Fore check node type
271
     *
272
     * @return NodeInterface
273
     */
274
    public function findNodeByPath(string $path = '', ?string $class = null): NodeInterface
275
    {
276
        if (empty($path) || '/' !== $path[0]) {
277
            $path = '/'.$path;
278
        }
279
280
        $last = strlen($path) - 1;
281
        if ('/' === $path[$last]) {
282
            $path = substr($path, 0, -1);
283
        }
284
285
        $parts = explode('/', $path);
286
        $parent = $this->getRoot();
287
        array_shift($parts);
288
        foreach ($parts as $node) {
289
            $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE);
290
        }
291
292
        if (null !== $class && !($parent instanceof $class)) {
293
            throw new Exception('node is not instance of '.$class);
294
        }
295
296
        return $parent;
297
    }
298
299
    /**
300
     * Load nodes by id.
301
     *
302
     * @param array  $id
303
     * @param string $class   Force check node type
304
     * @param bool   $deleted
305
     *
306
     * @return Generator
307
     */
308
    public function findNodesById(array $id = [], ?string $class = null, int $deleted = NodeInterface::DELETED_INCLUDE): Generator
309
    {
310
        $find = [];
311
        foreach ($id as $i) {
312
            $find[] = new ObjectId($i);
313
        }
314
315
        $filter = [
316
            '_id' => ['$in' => $find],
317
        ];
318
319
        switch ($deleted) {
320
            case NodeInterface::DELETED_INCLUDE:
321
                break;
322
            case NodeInterface::DELETED_EXCLUDE:
323
                $filter['deleted'] = false;
324
325
                break;
326
            case NodeInterface::DELETED_ONLY:
327
                $filter['deleted'] = ['$type' => 9];
328
329
                break;
330
        }
331
332
        $result = $this->db->storage->find($filter);
333
334
        $nodes = [];
0 ignored issues
show
Unused Code introduced by
$nodes 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...
335
        foreach ($result as $node) {
336
            $return = $this->initNode($node);
337
338
            if (null !== $class && !($return instanceof $class)) {
339
                throw new Exception('node is not an instance of '.$class);
340
            }
341
342
            yield $return;
343
        }
344
    }
345
346
    /**
347
     * Load nodes by id.
348
     *
349
     * @param array  $path
350
     * @param string $class Force check node type
351
     *
352
     * @return Generator
353
     */
354
    public function findNodesByPath(array $path = [], ?string $class = null): Generator
355
    {
356
        $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...
357
        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...
358
            if (empty($path) || '/' !== $path[0]) {
359
                $path = '/'.$path;
360
            }
361
362
            $last = strlen($path) - 1;
363
            if ('/' === $path[$last]) {
364
                $path = substr($path, 0, -1);
365
            }
366
367
            $parts = explode('/', $path);
368
            $parent = $this->getRoot();
369
            array_shift($parts);
370
            foreach ($parts as $node) {
371
                $parent = $parent->getChild($node, NodeInterface::DELETED_EXCLUDE);
372
            }
373
374
            if (null !== $class && !($parent instanceof $class)) {
375
                throw new Exception('node is not an instance of '.$class);
376
            }
377
378
            yield $parent;
379
        }
380
    }
381
382
    /**
383
     * Load nodes by id.
384
     *
385
     * @param array  $id
386
     * @param array  $path
387
     * @param string $class   Force set node type
388
     * @param int    $deleted
389
     *
390
     * @return Generator
391
     */
392
    public function getNodes(?array $id = null, ?array $path = null, $class = null, int $deleted = NodeInterface::DELETED_EXCLUDE): Generator
393
    {
394
        if (null === $id && null === $path) {
395
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
396
        }
397
        if (null !== $id && null !== $path) {
398
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
399
        }
400
        if (null !== $id) {
401
            if (null === $deleted) {
402
                $deleted = NodeInterface::DELETED_INCLUDE;
403
            }
404
405
            return $this->findNodesById($id, $class, $deleted);
406
        }
407
        if (null !== $path) {
408
            if (null === $deleted) {
409
                $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...
410
            }
411
412
            return $this->findNodesByPath($path, $class);
413
        }
414
    }
415
416
    /**
417
     * Load node.
418
     *
419
     * @param string $id
420
     * @param string $path
421
     * @param string $class      Force set node type
422
     * @param bool   $multiple   Allow $id to be an array
423
     * @param bool   $allow_root Allow instance of root collection
424
     * @param bool   $deleted    How to handle deleted node
425
     *
426
     * @return NodeInterface
427
     */
428
    public function getNode($id = null, $path = null, $class = null, bool $multiple = false, bool $allow_root = false, int $deleted = NodeInterface::DELETED_EXCLUDE): NodeInterface
429
    {
430
        if (empty($id) && empty($path)) {
431
            if (true === $allow_root) {
432
                return $this->getRoot();
433
            }
434
435
            throw new Exception\InvalidArgument('neither parameter id nor p (path) was given');
436
        }
437
        if (null !== $id && null !== $path) {
438
            throw new Exception\InvalidArgument('parameter id and p (path) can not be used at the same time');
439
        }
440
        if (null !== $id) {
441
            if (null === $deleted) {
442
                $deleted = NodeInterface::DELETED_INCLUDE;
443
            }
444
445
            if (true === $multiple && is_array($id)) {
446
                return $this->findNodesById($id, $class, $deleted);
447
            }
448
449
            return $this->findNodeById($id, $class, $deleted);
450
        }
451
        if (null !== $path) {
452
            if (null === $deleted) {
453
                $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...
454
            }
455
456
            return $this->findNodeByPath($path, $class);
457
        }
458
    }
459
460
    /**
461
     * Find node with custom filter.
462
     *
463
     * @param array $filter
464
     *
465
     * @return NodeInterface
466
     */
467
    public function findNodeByFilter(array $filter): NodeInterface
468
    {
469
        $result = $this->db->storage->findOne($filter);
470
        if (null === $result) {
471
            throw new Exception\NotFound(
472
                'node with custom filter was not found',
473
                Exception\NotFound::NODE_NOT_FOUND
474
            );
475
        }
476
477
        return $this->initNode($result);
478
    }
479
480
    /**
481
     * Find nodes with custom filters.
482
     *
483
     * @param array $filter
484
     *
485
     * @return Generator
486
     */
487
    public function findNodesByFilter(array $filter): Generator
488
    {
489
        $result = $this->db->storage->find($filter);
490
        $list = [];
0 ignored issues
show
Unused Code introduced by
$list 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...
491
492
        foreach ($result as $node) {
493
            try {
494
                yield $this->initNode($node);
495
            } catch (\Exception $e) {
496
                $this->logger->info('remove node from result list, failed load node', [
497
                    'category' => get_class($this),
498
                    'exception' => $e,
499
                ]);
500
            }
501
        }
502
    }
503
504
    /**
505
     * Get custom filtered children.
506
     *
507
     * @param int   $deleted
508
     * @param array $filter
509
     *
510
     * @return Generator
511
     */
512
    public function findNodesByFilterUser(int $deleted, array $filter): Generator
513
    {
514
        $shares = $this->user->getShares();
515
        $stored_filter = ['$and' => [
516
            [],
517
            ['$or' => [
518
                ['owner' => $this->user->getId()],
519
                ['shared' => ['$in' => $shares]],
520
            ]],
521
        ]];
522
523
        if (NodeInterface::DELETED_EXCLUDE === $deleted) {
524
            $stored_filter['$and'][0]['deleted'] = false;
525
        } elseif (NodeInterface::DELETED_ONLY === $deleted) {
526
            $stored_filter['$and'][0]['deleted'] = ['$type' => 9];
527
        }
528
529
        $stored_filter['$and'][0] = array_merge($filter, $stored_filter['$and'][0]);
530
        $result = $this->db->storage->find($stored_filter);
531
532
        foreach ($result as $node) {
533
            try {
534
                yield $this->initNode($node);
535
            } catch (\Exception $e) {
536
                $this->logger->info('remove node from result list, failed load node', [
537
                    'category' => get_class($this),
538
                    'exception' => $e,
539
                ]);
540
            }
541
        }
542
    }
543
544
    /**
545
     * Init node.
546
     *
547
     * @param array $node
548
     *
549
     * @return NodeInterface
550
     */
551
    public function initNode(array $node): NodeInterface
552
    {
553
        if (isset($node['shared']) && true === $node['shared'] && null !== $this->user && $node['owner'] != $this->user->getId()) {
554
            $node = $this->findReferenceNode($node);
555
        }
556
557
        //this would result in a recursiv call until the top level node
558
        /*if (isset($node['parent'])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
559
            try {
560
                $this->findNodeById($node['parent']);
561
            } catch (Exception\InvalidArgument $e) {
562
                throw new Exception\InvalidArgument('invalid parent node specified: '.$e->getMessage());
563
            } catch (Exception\NotFound $e) {
564
                throw new Exception\InvalidArgument('invalid parent node specified: '.$e->getMessage());
565
            }
566
        }*/
567
568
        if (!array_key_exists('directory', $node)) {
569
            throw new Exception('invalid node ['.$node['_id'].'] found, directory attribute does not exists');
570
        }
571
        if (true === $node['directory']) {
572
            $instance = new Collection($node, $this, $this->logger, $this->hook, $this->acl);
573
        } else {
574
            $instance = new File($node, $this, $this->logger, $this->hook, $this->acl, $this->storage);
575
        }
576
577
        if (!$this->acl->isAllowed($instance, 'r')) {
578
            if ($instance->isReference()) {
579
                $instance->delete(true);
580
            }
581
582
            throw new ForbiddenException(
583
                'not allowed to access node',
584
                ForbiddenException::NOT_ALLOWED_TO_ACCESS
585
            );
586
        }
587
588
        if (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...
589
            $this->logger->info('node ['.$node['_id'].'] is not accessible anmyore, destroy node cause of expired destroy flag', [
590
                'category' => get_class($this),
591
            ]);
592
593
            $instance->delete(true);
594
595
            throw new Exception\Conflict('node is not available anymore');
596
        }
597
598
        $this->cache[(string) $node['_id']] = $instance;
599
600
        return $instance;
601
    }
602
603
    /**
604
     * Resolve shared node to reference or share depending who requested.
605
     *
606
     * @param array $node
607
     *
608
     * @return array
609
     */
610
    protected function findReferenceNode(array $node): array
611
    {
612
        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...
613
            $this->logger->debug('reference node ['.$node['_id'].'] requested from share owner, trying to find the shared node', [
614
                'category' => get_class($this),
615
            ]);
616
617
            $node = $this->db->storage->findOne([
618
                'owner' => $this->user->getId(),
619
                'shared' => true,
620
                '_id' => $node['reference'],
621
            ]);
622
623
            if (null === $node) {
624
                throw new Exception\NotFound(
625
                    'no share node for reference node '.$node['reference'].' found',
626
                    Exception\NotFound::SHARE_NOT_FOUND
627
                );
628
            }
629
        } else {
630
            $this->logger->debug('share node ['.$node['_id'].'] requested from member, trying to find the reference node', [
631
                'category' => get_class($this),
632
            ]);
633
634
            $node = $this->db->storage->findOne([
635
                'owner' => $this->user->getId(),
636
                'shared' => true,
637
                'reference' => $node['_id'],
638
            ]);
639
640
            if (null === $node) {
641
                throw new Exception\NotFound(
642
                    'no share reference for node '.$node['_id'].' found',
643
                    Exception\NotFound::REFERENCE_NOT_FOUND
644
                );
645
            }
646
        }
647
648
        return $node;
649
    }
650
}
651