Passed
Push — master ( fa5669...b4c5b9 )
by Raffael
10:59
created

AbstractNode::getPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
crap 6
eloc 6
nc 2
nop 0
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\Filesystem\Node;
13
14
use Balloon\App\AppInterface;
15
use Balloon\Filesystem;
16
use Balloon\Filesystem\Acl;
17
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
18
use Balloon\Filesystem\Exception;
19
use Balloon\Filesystem\Storage;
20
use Balloon\Hook;
21
use Balloon\Server;
22
use Balloon\Server\User;
23
use MongoDB\BSON\ObjectId;
24
use MongoDB\BSON\UTCDateTime;
25
use MongoDB\Database;
26
use Normalizer;
27
use Psr\Log\LoggerInterface;
28
use ZipStream\ZipStream;
29
30
abstract class AbstractNode implements NodeInterface
31
{
32
    /**
33
     * name max lenght.
34
     */
35
    const MAX_NAME_LENGTH = 255;
36
37
    /**
38
     * Unique id.
39
     *
40
     * @var ObjectId
41
     */
42
    protected $_id;
43
44
    /**
45
     * Node name.
46
     *
47
     * @var string
48
     */
49
    protected $name = '';
50
51
    /**
52
     * Owner.
53
     *
54
     * @var ObjectId
55
     */
56
    protected $owner;
57
58
    /**
59
     * Mime.
60
     *
61
     * @var string
62
     */
63
    protected $mime;
64
65
    /**
66
     * Meta attributes.
67
     *
68
     * @var array
69
     */
70
    protected $meta = [];
71
72
    /**
73
     * Parent collection.
74
     *
75
     * @var ObjectId
76
     */
77
    protected $parent;
78
79
    /**
80
     * Is file deleted.
81
     *
82
     * @var bool|UTCDateTime
83
     */
84
    protected $deleted = false;
85
86
    /**
87
     * Is shared?
88
     *
89
     * @var bool
90
     */
91
    protected $shared = false;
92
93
    /**
94
     * Destory at a certain time.
95
     *
96
     * @var UTCDateTime
97
     */
98
    protected $destroy;
99
100
    /**
101
     * Changed timestamp.
102
     *
103
     * @var UTCDateTime
104
     */
105
    protected $changed;
106
107
    /**
108
     * Created timestamp.
109
     *
110
     * @var UTCDateTime
111
     */
112
    protected $created;
113
114
    /**
115
     * Point to antother node (Means this node is reference to $reference).
116
     *
117
     * @var ObjectId
118
     */
119
    protected $reference;
120
121
    /**
122
     * Raw attributes before any processing or modifications.
123
     *
124
     * @var array
125
     */
126
    protected $raw_attributes;
127
128
    /**
129
     * Readonly flag.
130
     *
131
     * @var bool
132
     */
133
    protected $readonly = false;
134
135
    /**
136
     * App attributes.
137
     *
138
     * @var array
139
     */
140
    protected $app = [];
141
142
    /**
143
     * Filesystem.
144
     *
145
     * @var Filesystem
146
     */
147
    protected $_fs;
148
149
    /**
150
     * Database.
151
     *
152
     * @var Database
153
     */
154
    protected $_db;
155
156
    /**
157
     * User.
158
     *
159
     * @var User
160
     */
161
    protected $_user;
162
163
    /**
164
     * Logger.
165
     *
166
     * @var LoggerInterface
167
     */
168
    protected $_logger;
169
170
    /**
171
     * Server.
172
     *
173
     * @var Server
174
     */
175
    protected $_server;
176
177
    /**
178
     * Hook.
179
     *
180
     * @var Hook
181
     */
182
    protected $_hook;
183
184
    /**
185
     * Acl.
186
     *
187
     * @var Acl
188
     */
189
    protected $_acl;
190
191
    /**
192
     * Storage adapter.
193
     *
194
     * @var string
195
     */
196
    protected $storage_adapter;
197
198
    /**
199
     * Acl.
200
     *
201
     * @var Acl
202
     */
203
    protected $acl;
204
205
    /**
206
     * Convert to filename.
207
     *
208
     * @return string
209
     */
210
    public function __toString()
211
    {
212
        return $this->name;
213
    }
214
215
    /**
216
     * Get owner.
217
     *
218
     * @return ObjectId
219
     */
220
    public function getOwner(): ObjectId
221
    {
222
        return $this->owner;
223
    }
224
225
    /**
226
     * Set filesystem.
227
     *
228
     * @param Filesystem $fs
229
     *
230
     * @return NodeInterface
231
     */
232
    public function setFilesystem(Filesystem $fs): NodeInterface
233
    {
234
        $this->_fs = $fs;
235
        $this->_user = $fs->getUser();
236
237
        return $this;
238
    }
239
240
    /**
241
     * Get filesystem.
242
     *
243
     * @return Filesystem
244
     */
245
    public function getFilesystem(): Filesystem
246
    {
247
        return $this->_fs;
248
    }
249
250
    /**
251
     * Check if $node is a sub node of any parent nodes of this node.
252
     *
253
     * @param NodeInterface $node
254
     *
255
     * @return bool
256
     */
257
    public function isSubNode(NodeInterface $node): bool
258
    {
259
        if ($node->getId() == $this->_id) {
260
            return true;
261
        }
262
263
        foreach ($node->getParents() as $node) {
264
            if ($node->getId() == $this->_id) {
265
                return true;
266
            }
267
        }
268
269
        if ($this->isRoot()) {
270
            return true;
271
        }
272
273
        return false;
274
    }
275
276
    /**
277
     * Move node.
278
     *
279
     * @param Collection $parent
280
     * @param int        $conflict
281
     *
282
     * @return NodeInterface
283
     */
284
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
285
    {
286
        if ($this->parent === $parent->getId()) {
287
            throw new Exception\Conflict(
288
                'source node '.$this->name.' is already in the requested parent folder',
289
                Exception\Conflict::ALREADY_THERE
290
            );
291
        }
292
        if ($this->isSubNode($parent)) {
293
            throw new Exception\Conflict(
294
                'node called '.$this->name.' can not be moved into itself',
295
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
296
            );
297
        }
298
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
299
            throw new ForbiddenException(
300
                'not allowed to move node '.$this->name,
301
                ForbiddenException::NOT_ALLOWED_TO_MOVE
302
            );
303
        }
304
305
        $exists = $parent->childExists($this->name);
306
        if (true === $exists && NodeInterface::CONFLICT_NOACTION === $conflict) {
307
            throw new Exception\Conflict(
308
                'a node called '.$this->name.' does already exists in this collection',
309
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
310
            );
311
        }
312
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
313
            throw new Exception\Conflict(
314
                'a shared folder can not be a child of a shared folder too',
315
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
316
            );
317
        }
318
        if ($parent->isDeleted()) {
319
            throw new Exception\Conflict(
320
                'cannot move node into a deleted collction',
321
                Exception\Conflict::DELETED_PARENT
322
            );
323
        }
324
325
        if (true === $exists && NodeInterface::CONFLICT_RENAME === $conflict) {
326
            $this->setName($this->getDuplicateName());
327
            $this->raw_attributes['name'] = $this->name;
328
        }
329
330
        if ($this instanceof Collection) {
331
            $this->getChildrenRecursive($this->getRealId(), $shares);
0 ignored issues
show
Bug introduced by
The variable $shares does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
332
333
            if (!empty($shares) && $parent->isShared()) {
334
                throw new Exception\Conflict(
335
                    'folder contains a shared folder',
336
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
337
                );
338
            }
339
        }
340
341
        if ($parent->isSpecial() && $this->shared !== $parent->getShareId() || !$parent->isSpecial() && $this->isShareMember()) {
342
            $new = $this->copyTo($parent, $conflict);
343
            $this->delete();
344
345
            return $new;
346
        }
347
348
        if (true === $exists && NodeInterface::CONFLICT_MERGE === $conflict) {
349
            $new = $this->copyTo($parent, $conflict);
350
            $this->delete(true/*, false, false*/);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
351
352
            return $new;
353
        }
354
355
        $this->parent = $parent->getRealId();
356
        $this->owner = $this->_user->getId();
357
358
        $this->save(['parent', 'shared', 'owner']);
359
360
        return $this;
361
    }
362
363
    /**
364
     * Get share id.
365
     *
366
     * @param bool $reference
367
     *
368
     * @return ObjectId
369
     */
370
    public function getShareId(bool $reference = false): ?ObjectId
371
    {
372
        if ($this->isReference() && true === $reference) {
373
            return $this->_id;
374
        }
375
        if ($this->isShareMember() && true === $reference) {
376
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type declared by the interface Balloon\Filesystem\Node\NodeInterface::getShareId of type MongoDB\BSON\ObjectId.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
377
        }
378
        if ($this->isShared() && $this->isReference()) {
379
            return $this->reference;
380
        }
381
        if ($this->isShared()) {
382
            return $this->_id;
383
        }
384
        if ($this->isShareMember()) {
385
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type declared by the interface Balloon\Filesystem\Node\NodeInterface::getShareId of type MongoDB\BSON\ObjectId.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
386
        }
387
388
        return null;
389
    }
390
391
    /**
392
     * Get share node.
393
     *
394
     * @return Collection
395
     */
396
    public function getShareNode(): ?Collection
397
    {
398
        if ($this->isShare()) {
399
            return $this;
400
        }
401
402
        if ($this->isSpecial()) {
403
            return $this->_fs->findNodeById($this->getShareId(true));
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId(true) can be null; however, findNodeById() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
404
        }
405
406
        return null;
407
    }
408
409
    /**
410
     * Is node marked as readonly?
411
     *
412
     * @return bool
413
     */
414
    public function isReadonly(): bool
415
    {
416
        return $this->readonly;
417
    }
418
419
    /**
420
     * Request is from node owner?
421
     *
422
     * @return bool
423
     */
424
    public function isOwnerRequest(): bool
425
    {
426
        return null !== $this->_user && $this->owner == $this->_user->getId();
427
    }
428
429
    /**
430
     * Check if node is kind of special.
431
     *
432
     * @return bool
433
     */
434
    public function isSpecial(): bool
435
    {
436
        if ($this->isShared()) {
437
            return true;
438
        }
439
        if ($this->isReference()) {
440
            return true;
441
        }
442
        if ($this->isShareMember()) {
443
            return true;
444
        }
445
446
        return false;
447
    }
448
449
    /**
450
     * Check if node is a sub node of a share.
451
     *
452
     * @return bool
453
     */
454
    public function isShareMember(): bool
455
    {
456
        return $this->shared instanceof ObjectId && !$this->isReference();
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...
457
    }
458
459
    /**
460
     * Is share.
461
     *
462
     * @return bool
463
     */
464
    public function isShare(): bool
465
    {
466
        return true === $this->shared && !$this->isReference();
467
    }
468
469
    /**
470
     * Is share (Reference or master share).
471
     *
472
     * @return bool
473
     */
474
    public function isShared(): bool
475
    {
476
        if (true === $this->shared) {
477
            return true;
478
        }
479
480
        return false;
481
    }
482
483
    /**
484
     * Set the name.
485
     *
486
     * @param string $name
487
     *
488
     * @return bool
489
     */
490
    public function setName($name): bool
491
    {
492
        $name = $this->checkName($name);
493
494
        try {
495
            $child = $this->getParent()->getChild($name);
496
            if ($child->getId() != $this->_id) {
497
                throw new Exception\Conflict(
498
                    'a node called '.$name.' does already exists in this collection',
499
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
500
                );
501
            }
502
        } catch (Exception\NotFound $e) {
503
            //child does not exists, we can safely rename
504
        }
505
506
        $this->name = $name;
507
508
        return $this->save('name');
509
    }
510
511
    /**
512
     * Check name.
513
     *
514
     * @param string $name
515
     *
516
     * @return string
517
     */
518
    public function checkName(string $name): string
519
    {
520
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
521
            throw new Exception\InvalidArgument('name contains invalid characters');
522
        }
523
        if (strlen($name) > self::MAX_NAME_LENGTH) {
524
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
525
        }
526
527
        if (!Normalizer::isNormalized($name)) {
528
            $name = Normalizer::normalize($name);
529
        }
530
531
        return $name;
532
    }
533
534
    /**
535
     * Get the name.
536
     *
537
     * @return string
538
     */
539
    public function getName(): string
540
    {
541
        return $this->name;
542
    }
543
544
    /**
545
     * Undelete.
546
     *
547
     * @param int    $conflict
548
     * @param string $recursion
549
     * @param bool   $recursion_first
550
     *
551
     * @return bool
552
     */
553
    public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool
554
    {
555
        if (!$this->_acl->isAllowed($this, 'w')) {
556
            throw new ForbiddenException(
557
                'not allowed to restore node '.$this->name,
558
                ForbiddenException::NOT_ALLOWED_TO_UNDELETE
559
            );
560
        }
561
        if (!$this->isDeleted()) {
562
            throw new Exception\Conflict(
563
                'node is not deleted, skip restore',
564
                Exception\Conflict::NOT_DELETED
565
            );
566
        }
567
568
        $parent = $this->getParent();
569
        if ($parent->isDeleted()) {
570
            throw new Exception\Conflict(
571
                'could not restore node '.$this->name.' into a deleted parent',
572
                Exception\Conflict::DELETED_PARENT
573
            );
574
        }
575
576
        if ($parent->childExists($this->name)) {
577
            if (NodeInterface::CONFLICT_MERGE === $conflict) {
578
                $this->copyTo($parent, $conflict);
579
                $this->delete(true);
580
            } elseif (NodeInterface::CONFLICT_RENAME === $conflict) {
581
                $this->setName($this->getDuplicateName());
582
                $this->raw_attributes['name'] = $this->name;
583
            } else {
584
                throw new Exception\Conflict(
585
                    'a node called '.$this->name.' does already exists in this collection',
586
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
587
                );
588
            }
589
        }
590
591
        if (null === $recursion) {
592
            $recursion_first = true;
593
            $recursion = uniqid();
594
        } else {
595
            $recursion_first = false;
596
        }
597
598
        $this->deleted = false;
599
600
        if ($this instanceof File) {
601
            $current = $this->version;
0 ignored issues
show
Unused Code introduced by
$current 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...
602
            $new = $this->increaseVersion();
603
604
            $this->history[] = [
605
                'version' => $new,
606
                'changed' => $this->changed,
607
                'user' => $this->owner,
608
                'type' => File::HISTORY_UNDELETE,
609
                'storage' => $this->storage,
610
                'storage_adapter' => $this->storage_adapter,
611
                'size' => $this->size,
612
                'mime' => $this->mime,
613
            ];
614
615
            return $this->save([
616
                'name',
617
                'deleted',
618
                'history',
619
                'version',
620
            ], [], $recursion, $recursion_first);
621
        }
622
623
        $this->save([
624
                'name',
625
                'deleted',
626
            ], [], $recursion, $recursion_first);
627
628
        if ($this->isReference()) {
629
            return true;
630
        }
631
632
        return $this->doRecursiveAction(function ($node) use ($conflict, $recursion) {
633
            $node->undelete($conflict, $recursion, false);
634
        }, NodeInterface::DELETED_ONLY);
635
    }
636
637
    /**
638
     * Is node deleted?
639
     *
640
     * @return bool
641
     */
642
    public function isDeleted(): bool
643
    {
644
        return $this->deleted instanceof UTCDateTime;
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...
645
    }
646
647
    /**
648
     * Get last modified timestamp.
649
     *
650
     * @return int
651
     */
652
    public function getLastModified(): int
653
    {
654
        if ($this->changed instanceof UTCDateTime) {
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...
655
            return (int) $this->changed->toDateTime()->format('U');
656
        }
657
658
        return 0;
659
    }
660
661
    /**
662
     * Get unique id.
663
     *
664
     * @return ObjectId
665
     */
666 1
    public function getId(): ?ObjectId
667
    {
668 1
        return $this->_id;
669
    }
670
671
    /**
672
     * Get parent.
673
     *
674
     * @return Collection
675
     */
676
    public function getParent(): ?Collection
677
    {
678
        try {
679
            if ($this->isRoot()) {
680
                return null;
681
            }
682
            if ($this->isInRoot()) {
683
                return $this->_fs->getRoot();
684
            }
685
686
            $parent = $this->_fs->findNodeById($this->parent);
687
688
            if ($parent->isShare() && !$parent->isOwnerRequest() && null !== $this->_user) {
689
                $node = $this->_db->storage->findOne([
690
                        'owner' => $this->_user->getId(),
691
                        'shared' => true,
692
                        'reference' => $this->parent,
693
                    ]);
694
695
                return $this->_fs->initNode($node);
696
            }
697
698
            return $parent;
699
        } catch (Exception\NotFound $e) {
700
            throw new Exception\NotFound(
701
                'parent node '.$this->parent.' could not be found',
702
                Exception\NotFound::PARENT_NOT_FOUND
703
            );
704
        }
705
    }
706
707
    /**
708
     * Get parents.
709
     *
710
     * @param array $parents
711
     *
712
     * @return array
713
     */
714
    public function getParents(?NodeInterface $node = null, array $parents = []): array
715
    {
716
        if (null === $node) {
717
            $node = $this;
718
        }
719
720
        if ($node->isInRoot()) {
721
            return $parents;
722
        }
723
        $parent = $node->getParent();
724
        $parents[] = $parent;
725
726
        return $node->getParents($parent, $parents);
727
    }
728
729
    /**
730
     * Get as zip.
731
     */
732
    public function getZip(): void
733
    {
734
        $archive = new ZipStream($this->name.'.zip');
735
        $this->zip($archive, false);
736
        $archive->finish();
737
    }
738
739
    /**
740
     * Create zip.
741
     *
742
     * @param ZipStream     $archive
743
     * @param bool          $self    true means that the zip represents the collection itself instead a child of the zip
744
     * @param NodeInterface $parent
745
     * @param string        $path
746
     * @param int           $depth
747
     *
748
     * @return bool
749
     */
750
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
751
    {
752
        if (null === $parent) {
753
            $parent = $this;
754
        }
755
756
        if ($parent instanceof Collection) {
757
            $children = $parent->getChildNodes();
758
759
            if (true === $self && 0 === $depth) {
760
                $path = $parent->getName().DIRECTORY_SEPARATOR;
761
            } elseif (0 === $depth) {
762
                $path = '';
763
            } elseif (0 !== $depth) {
764
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
765
            }
766
767
            foreach ($children as $child) {
768
                $name = $path.$child->getName();
769
770
                if ($child instanceof Collection) {
771
                    $this->zip($archive, $self, $child, $name, ++$depth);
772
                } elseif ($child instanceof File) {
773
                    try {
774
                        $resource = $child->get();
775
                        if ($resource !== null) {
776
                            $archive->addFileFromStream($name, $resource);
777
                        }
778
                    } catch (\Exception $e) {
779
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
780
                            'category' => get_class($this),
781
                            'exception' => $e,
782
                        ]);
783
                    }
784
                }
785
            }
786
        } elseif ($parent instanceof File) {
787
            $resource = $parent->get();
788
            if ($resource !== null) {
789
                $archive->addFileFromStream($parent->getName(), $resource);
790
            }
791
        }
792
793
        return true;
794
    }
795
796
    /**
797
     * Get mime type.
798
     *
799
     * @return string
800
     */
801 1
    public function getContentType(): string
802
    {
803 1
        return $this->mime;
804
    }
805
806
    /**
807
     * Is reference.
808
     *
809
     *  @return bool
810
     */
811
    public function isReference(): bool
812
    {
813
        return $this->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...
814
    }
815
816
    /**
817
     * Set app attributes.
818
     *
819
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
820
     * @param array        $attributes
821
     *
822
     * @return NodeInterface
823
     */
824
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
825
    {
826
        $this->app[$namespace] = $attributes;
827
        $this->save('app.'.$namespace);
828
829
        return $this;
830
    }
831
832
    /**
833
     * Set app attribute.
834
     *
835
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
836
     * @param string       $attribute
837
     * @param mixed        $value
838
     *
839
     * @return NodeInterface
840
     */
841
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
842
    {
843
        if (!isset($this->app[$namespace])) {
844
            $this->app[$namespace] = [];
845
        }
846
847
        $this->app[$namespace][$attribute] = $value;
848
        $this->save('app.'.$namespace);
849
850
        return $this;
851
    }
852
853
    /**
854
     * Remove app attribute.
855
     *
856
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
857
     *
858
     * @return NodeInterface
859
     */
860
    public function unsetAppAttributes(string $namespace): NodeInterface
861
    {
862
        if (isset($this->app[$namespace])) {
863
            unset($this->app[$namespace]);
864
            $this->save('app.'.$namespace);
865
        }
866
867
        return $this;
868
    }
869
870
    /**
871
     * Remove app attribute.
872
     *
873
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
874
     * @param string       $attribute
875
     *
876
     * @return NodeInterface
877
     */
878
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
879
    {
880
        if (isset($this->app[$namespace][$attribute])) {
881
            unset($this->app[$namespace][$attribute]);
882
            $this->save('app'.$namespace);
883
        }
884
885
        return $this;
886
    }
887
888
    /**
889
     * Get app attribute.
890
     *
891
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
892
     * @param string       $attribute
893
     *
894
     * @return mixed
895
     */
896
    public function getAppAttribute(string $namespace, string $attribute)
897
    {
898
        if (isset($this->app[$namespace][$attribute])) {
899
            return $this->app[$namespace][$attribute];
900
        }
901
902
        return null;
903
    }
904
905
    /**
906
     * Get app attributes.
907
     *
908
     * @param AppInterface $app
0 ignored issues
show
Bug introduced by
There is no parameter named $app. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
909
     *
910
     * @return array
911
     */
912
    public function getAppAttributes(string $namespace): array
913
    {
914
        if (isset($this->app[$namespace])) {
915
            return $this->app[$namespace];
916
        }
917
918
        return [];
919
    }
920
921
    /**
922
     * Set meta attributes.
923
     *
924
     * @param array $attributes
925
     *
926
     * @return NodeInterface
927
     */
928
    public function setMetaAttributes(array $attributes): NodeInterface
929
    {
930
        $attributes = $this->validateMetaAttributes($attributes);
931
        foreach ($attributes as $attribute => $value) {
932
            if (empty($value) && isset($this->meta[$attribute])) {
933
                unset($this->meta[$attribute]);
934
            } elseif (!empty($value)) {
935
                $this->meta[$attribute] = $value;
936
            }
937
        }
938
939
        $this->save('meta');
940
941
        return $this;
942
    }
943
944
    /**
945
     * Get meta attributes as array.
946
     *
947
     * @param array $attribute Specify attributes to return
0 ignored issues
show
Documentation introduced by
There is no parameter named $attribute. Did you maybe mean $attributes?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
948
     *
949
     * @return array
950
     */
951
    public function getMetaAttributes(array $attributes = []): array
952
    {
953
        if (empty($attributes)) {
954
            return $this->meta;
955
        }
956
        if (is_array($attributes)) {
957
            return array_intersect_key($this->meta, array_flip($attributes));
958
        }
959
    }
960
961
    /**
962
     * Mark node as readonly.
963
     *
964
     * @param bool $readonly
965
     *
966
     * @return bool
967
     */
968
    public function setReadonly(bool $readonly = true): bool
969
    {
970
        $this->readonly = $readonly;
971
972
        return $this->save('readonly');
973
    }
974
975
    /**
976
     * Mark node as self-destroyable.
977
     *
978
     * @param UTCDateTime $ts
979
     *
980
     * @return bool
981
     */
982
    public function setDestroyable(?UTCDateTime $ts): bool
983
    {
984
        $this->destroy = $ts;
985
986
        if (null === $ts) {
987
            return $this->save([], 'destroy');
988
        }
989
990
        return $this->save('destroy');
991
    }
992
993
    /**
994
     * Get original raw attributes before any processing.
995
     *
996
     * @return array
997
     */
998
    public function getRawAttributes(): array
999
    {
1000
        return $this->raw_attributes;
1001
    }
1002
1003
    /**
1004
     * Check if node is in root.
1005
     *
1006
     * @return bool
1007
     */
1008
    public function isInRoot(): bool
1009
    {
1010
        return null === $this->parent;
1011
    }
1012
1013
    /**
1014
     * Check if node is an instance of the actual root collection.
1015
     *
1016
     * @return bool
1017
     */
1018
    public function isRoot(): bool
1019
    {
1020
        return null === $this->_id && ($this instanceof Collection);
1021
    }
1022
1023
    /**
1024
     * Resolve node path.
1025
     *
1026
     * @return string
1027
     */
1028
    public function getPath(): string
1029
    {
1030
        $path = '';
1031
        foreach (array_reverse($this->getParents()) as $parent) {
1032
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1033
        }
1034
1035
        $path .= DIRECTORY_SEPARATOR.$this->getName();
0 ignored issues
show
Bug introduced by
Consider using $this->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
1036
1037
        return $path;
1038
    }
1039
1040
    /**
1041
     * Save node attributes.
1042
     *
1043
     * @param array|string $attributes
1044
     * @param array|string $remove
1045
     * @param string       $recursion
1046
     * @param bool         $recursion_first
1047
     *
1048
     * @return bool
1049
     */
1050
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
1051
    {
1052
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
1053
            throw new ForbiddenException(
1054
                'not allowed to modify node '.$this->name,
1055
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
1056
            );
1057
        }
1058
1059
        if ($this instanceof Collection && $this->isRoot()) {
1060
            return false;
1061
        }
1062
1063
        $remove = (array) $remove;
1064
        $attributes = (array) $attributes;
1065
        $this->_hook->run(
1066
            'preSaveNodeAttributes',
1067
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
1068
        );
1069
1070
        try {
1071
            $set = [];
1072
            $values = $this->getAttributes();
1073
            foreach ($attributes as $attr) {
1074
                $set[$attr] = $this->getArrayValue($values, $attr);
1075
            }
1076
1077
            $update = [];
1078
            if (!empty($set)) {
1079
                $update['$set'] = $set;
1080
            }
1081
1082
            if (!empty($remove)) {
1083
                $remove = array_fill_keys($remove, 1);
1084
                $update['$unset'] = $remove;
1085
            }
1086
1087
            if (empty($update)) {
1088
                return false;
1089
            }
1090
            $result = $this->_db->storage->updateOne([
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...
1091
                    '_id' => $this->_id,
1092
                ], $update);
1093
1094
            $this->_hook->run(
1095
                'postSaveNodeAttributes',
1096
                [$this, $attributes, $remove, $recursion, $recursion_first]
1097
            );
1098
1099
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1100
                'category' => get_class($this),
1101
                'params' => $update,
1102
            ]);
1103
1104
            return true;
1105
        } catch (\Exception $e) {
1106
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1107
                'category' => get_class($this),
1108
                'exception' => $e,
1109
            ]);
1110
1111
            throw $e;
1112
        }
1113
    }
1114
1115
    /**
1116
     * Get array value via string path.
1117
     *
1118
     * @param iterable $arr
0 ignored issues
show
Bug introduced by
There is no parameter named $arr. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1119
     * @param string   $path
1120
     * @param string   $seperator
0 ignored issues
show
Bug introduced by
There is no parameter named $seperator. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1121
     *
1122
     * @return mixed
1123
     */
1124
    protected function getArrayValue(Iterable $array, string $path, string $separator = '.')
1125
    {
1126
        if (isset($array[$path])) {
1127
            return $array[$path];
1128
        }
1129
        $keys = explode($separator, $path);
1130
1131
        foreach ($keys as $key) {
1132
            if (!array_key_exists($key, $array)) {
1133
                return;
1134
            }
1135
1136
            $array = $array[$key];
1137
        }
1138
1139
        return $array;
1140
    }
1141
1142
    /**
1143
     * Validate meta attributes.
1144
     *
1145
     * @param array $attributes
1146
     *
1147
     * @return array
1148
     */
1149
    protected function validateMetaAttributes(array $attributes): array
1150
    {
1151
        foreach ($attributes as $attribute => $value) {
1152
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1153
            if (!defined($const)) {
1154
                throw new Exception('meta attribute '.$attribute.' is not valid');
1155
            }
1156
1157
            if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) {
1158
                throw new Exception('tags meta attribute must be an array of strings');
1159
            }
1160
1161
            if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) {
1162
                throw new Exception($attribute.' meta attribute must be a string');
1163
            }
1164
        }
1165
1166
        return $attributes;
1167
    }
1168
1169
    /**
1170
     * Duplicate name with a uniqid within name.
1171
     */
1172
    protected function getDuplicateName(?string $name = null, ?string $class = null): string
1173
    {
1174
        if (null === $name) {
1175
            $name = $this->name;
1176
        }
1177
1178
        if (null === $class) {
1179
            $class = get_class($this);
1180
        }
1181
1182
        if ($class === Collection::class) {
1183
            return $name.' ('.substr(uniqid('', true), -4).')';
1184
        }
1185
1186
        $ext = substr(strrchr($name, '.'), 1);
1187
        if (false === $ext) {
1188
            return $name.' ('.substr(uniqid('', true), -4).')';
1189
        }
1190
1191
        $name = substr($name, 0, -(strlen($ext) + 1));
1192
1193
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1194
    }
1195
1196
    /**
1197
     * Completly remove node.
1198
     *
1199
     * @return bool
1200
     */
1201
    abstract protected function _forceDelete(): bool;
1202
}
1203