Completed
Branch dev (bc6e47)
by Raffael
02:23
created

AbstractNode::save()   C

Complexity

Conditions 10
Paths 53

Size

Total Lines 64
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 64
rs 6.309
c 0
b 0
f 0
cc 10
eloc 39
nc 53
nop 4

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
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
10
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
11
 */
12
13
namespace Balloon\Filesystem\Node;
14
15
use Balloon\App\AppInterface;
16
use Balloon\Exception;
17
use Balloon\Filesystem;
18
use Balloon\Filesystem\Acl;
19
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
20
use Balloon\Filesystem\Storage;
21
use Balloon\Hook;
22
use Balloon\Server;
23
use Balloon\Server\User;
24
use MongoDB\BSON\ObjectId;
25
use MongoDB\BSON\UTCDateTime;
26
use MongoDB\Database;
27
use Normalizer;
28
use PHPZip\Zip\Stream\ZipStream;
29
use Psr\Log\LoggerInterface;
30
31
abstract class AbstractNode implements NodeInterface
32
{
33
    /**
34
     * name max lenght.
35
     */
36
    const MAX_NAME_LENGTH = 255;
37
38
    /**
39
     * Unique id.
40
     *
41
     * @var ObjectId
42
     */
43
    protected $_id;
44
45
    /**
46
     * Node name.
47
     *
48
     * @var string
49
     */
50
    protected $name = '';
51
52
    /**
53
     * Owner.
54
     *
55
     * @var ObjectId
56
     */
57
    protected $owner;
58
59
    /**
60
     * Mime.
61
     *
62
     * @var string
63
     */
64
    protected $mime;
65
66
    /**
67
     * Meta attributes.
68
     *
69
     * @var array
70
     */
71
    protected $meta = [];
72
73
    /**
74
     * Parent collection.
75
     *
76
     * @var ObjectId
77
     */
78
    protected $parent;
79
80
    /**
81
     * Is file deleted.
82
     *
83
     * @var bool|UTCDateTime
84
     */
85
    protected $deleted = false;
86
87
    /**
88
     * Is shared?
89
     *
90
     * @var bool
91
     */
92
    protected $shared = false;
93
94
    /**
95
     * Destory at a certain time.
96
     *
97
     * @var UTCDateTime
98
     */
99
    protected $destroy;
100
101
    /**
102
     * Changed timestamp.
103
     *
104
     * @var UTCDateTime
105
     */
106
    protected $changed;
107
108
    /**
109
     * Created timestamp.
110
     *
111
     * @var UTCDateTime
112
     */
113
    protected $created;
114
115
    /**
116
     * Point to antother node (Means this node is reference to $reference).
117
     *
118
     * @var ObjectId
119
     */
120
    protected $reference;
121
122
    /**
123
     * Raw attributes before any processing or modifications.
124
     *
125
     * @var array
126
     */
127
    protected $raw_attributes;
128
129
    /**
130
     * Readonly flag.
131
     *
132
     * @var bool
133
     */
134
    protected $readonly = false;
135
136
    /**
137
     * App attributes.
138
     *
139
     * @var array
140
     */
141
    protected $app = [];
142
143
    /**
144
     * Filesystem.
145
     *
146
     * @var Filesystem
147
     */
148
    protected $_fs;
149
150
    /**
151
     * Database.
152
     *
153
     * @var Database
154
     */
155
    protected $_db;
156
157
    /**
158
     * User.
159
     *
160
     * @var User
161
     */
162
    protected $_user;
163
164
    /**
165
     * Logger.
166
     *
167
     * @var LoggerInterface
168
     */
169
    protected $_logger;
170
171
    /**
172
     * Server.
173
     *
174
     * @var Server
175
     */
176
    protected $_server;
177
178
    /**
179
     * Hook.
180
     *
181
     * @var Hook
182
     */
183
    protected $_hook;
184
185
    /**
186
     * Acl.
187
     *
188
     * @var Acl
189
     */
190
    protected $_acl;
191
192
    /**
193
     * Storage adapter.
194
     *
195
     * @var string
196
     */
197
    protected $storage_adapter;
198
199
    /**
200
     * Acl.
201
     *
202
     * @var Acl
203
     */
204
    protected $acl;
205
206
    /**
207
     * Convert to filename.
208
     *
209
     * @return string
210
     */
211
    public function __toString()
212
    {
213
        return $this->name;
214
    }
215
216
    /**
217
     * Get owner.
218
     *
219
     * @return ObjectId
220
     */
221
    public function getOwner(): ObjectId
222
    {
223
        return $this->owner;
224
    }
225
226
    /**
227
     * Set filesystem.
228
     *
229
     * @param Filesystem $fs
230
     *
231
     * @return NodeInterface
232
     */
233
    public function setFilesystem(Filesystem $fs): NodeInterface
234
    {
235
        $this->_fs = $fs;
236
        $this->_user = $fs->getUser();
237
238
        return $this;
239
    }
240
241
    /**
242
     * Get filesystem.
243
     *
244
     * @return Filesystem
245
     */
246
    public function getFilesystem(): Filesystem
247
    {
248
        return $this->_fs;
249
    }
250
251
    /**
252
     * Check if $node is a sub node of any parent nodes of this node.
253
     *
254
     * @param NodeInterface $node
255
     *
256
     * @return bool
257
     */
258
    public function isSubNode(NodeInterface $node): bool
259
    {
260
        if ($node->getId() === $this->_id) {
261
            return true;
262
        }
263
264
        foreach ($node->getParents() as $node) {
265
            if ($node->getId() === $this->_id) {
266
                return true;
267
            }
268
        }
269
270
        if ($this->isRoot()) {
271
            return true;
272
        }
273
274
        return false;
275
    }
276
277
    /**
278
     * Move node.
279
     *
280
     * @param Collection $parent
281
     * @param int        $conflict
282
     *
283
     * @return NodeInterface
284
     */
285
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
286
    {
287
        if ($this->parent === $parent->getId()) {
288
            throw new Exception\Conflict(
289
                'source node '.$this->name.' is already in the requested parent folder',
290
                Exception\Conflict::ALREADY_THERE
291
            );
292
        }
293
        if ($this->isSubNode($parent)) {
294
            throw new Exception\Conflict(
295
                'node called '.$this->name.' can not be moved into itself',
296
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
297
            );
298
        }
299
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
300
            throw new ForbiddenException(
301
                'not allowed to move node '.$this->name,
302
                ForbiddenException::NOT_ALLOWED_TO_MOVE
303
            );
304
        }
305
306
        $exists = $parent->childExists($this->name);
307
        if (true === $exists && NodeInterface::CONFLICT_NOACTION === $conflict) {
308
            throw new Exception\Conflict(
309
                'a node called '.$this->name.' does already exists in this collection',
310
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
311
            );
312
        }
313
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
314
            throw new Exception\Conflict(
315
                'a shared folder can not be a child of a shared folder too',
316
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
317
            );
318
        }
319
        if ($parent->isDeleted()) {
320
            throw new Exception\Conflict(
321
                'cannot move node into a deleted collction',
322
                Exception\Conflict::DELETED_PARENT
323
            );
324
        }
325
326
        if (true === $exists && NodeInterface::CONFLICT_RENAME === $conflict) {
327
            $this->setName($this->getDuplicateName());
328
            $this->raw_attributes['name'] = $this->name;
329
        }
330
331
        if ($this instanceof Collection) {
332
            $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...
333
334
            if (!empty($shares) && $parent->isShared()) {
335
                throw new Exception\Conflict(
336
                    'folder contains a shared folder',
337
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
338
                );
339
            }
340
        }
341
342
        if ($parent->isSpecial() && $this->shared !== $parent->getShareId() || !$parent->isSpecial() && $this->isShareMember()) {
343
            $new = $this->copyTo($parent, $conflict);
344
            $this->delete();
345
346
            return $new;
347
        }
348
349
        if (true === $exists && NodeInterface::CONFLICT_MERGE === $conflict) {
350
            $new = $this->copyTo($parent, $conflict);
351
            $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...
352
353
            return $new;
354
        }
355
356
        $this->parent = $parent->getRealId();
357
        $this->owner = $this->_user->getId();
358
359
        $this->save(['parent', 'shared', 'owner']);
360
361
        return $this;
362
    }
363
364
    /**
365
     * Get share id.
366
     *
367
     * @param bool $reference
368
     *
369
     * @return ObjectId
370
     */
371
    public function getShareId(bool $reference = false): ?ObjectId
372
    {
373
        if ($this->isReference() && true === $reference) {
374
            return $this->_id;
375
        }
376
        if ($this->isShareMember() && true === $reference) {
377
            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...
378
        }
379
        if ($this->isShared() && $this->isReference()) {
380
            return $this->reference;
381
        }
382
        if ($this->isShared()) {
383
            return $this->_id;
384
        }
385
        if ($this->isShareMember()) {
386
            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...
387
        }
388
389
        return null;
390
    }
391
392
    /**
393
     * Get share node.
394
     *
395
     * @param bool $reference
0 ignored issues
show
Bug introduced by
There is no parameter named $reference. 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...
396
     *
397
     * @return Collection
398
     */
399
    public function getShareNode(): ?Collection
400
    {
401
        if ($this->isSpecial()) {
402
            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...
403
        }
404
405
        return null;
406
    }
407
408
    /**
409
     * Is node marked as readonly?
410
     *
411
     * @return bool
412
     */
413
    public function isReadonly(): bool
414
    {
415
        return $this->readonly;
416
    }
417
418
    /**
419
     * Request is from node owner?
420
     *
421
     * @return bool
422
     */
423
    public function isOwnerRequest(): bool
424
    {
425
        return null !== $this->_user && $this->owner == $this->_user->getId();
426
    }
427
428
    /**
429
     * Check if node is kind of special.
430
     *
431
     * @return bool
432
     */
433
    public function isSpecial(): bool
434
    {
435
        if ($this->isShared()) {
436
            return true;
437
        }
438
        if ($this->isReference()) {
439
            return true;
440
        }
441
        if ($this->isShareMember()) {
442
            return true;
443
        }
444
445
        return false;
446
    }
447
448
    /**
449
     * Check if node is a sub node of a share.
450
     *
451
     * @return bool
452
     */
453
    public function isShareMember(): bool
454
    {
455
        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...
456
    }
457
458
    /**
459
     * Is share.
460
     *
461
     * @return bool
462
     */
463
    public function isShare(): bool
464
    {
465
        return true === $this->shared && !$this->isReference();
466
    }
467
468
    /**
469
     * Is share (Reference or master share).
470
     *
471
     * @return bool
472
     */
473
    public function isShared(): bool
474
    {
475
        if (true === $this->shared) {
476
            return true;
477
        }
478
479
        return false;
480
    }
481
482
    /**
483
     * Set the name.
484
     *
485
     * @param string $name
486
     *
487
     * @return bool
488
     */
489
    public function setName($name): bool
490
    {
491
        $name = $this->checkName($name);
492
493
        try {
494
            $child = $this->getParent()->getChild($name);
495
            if ($child->getId() != $this->_id) {
496
                throw new Exception\Conflict(
497
                    'a node called '.$name.' does already exists in this collection',
498
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
499
                );
500
            }
501
        } catch (Exception\NotFound $e) {
502
            //child does not exists, we can safely rename
503
        }
504
505
        $this->name = $name;
506
507
        return $this->save('name');
508
    }
509
510
    /**
511
     * Check name.
512
     *
513
     * @param string $name
514
     *
515
     * @return string
516
     */
517
    public function checkName(string $name): string
518
    {
519
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
520
            throw new Exception\InvalidArgument('name contains invalid characters');
521
        }
522
        if (strlen($name) > self::MAX_NAME_LENGTH) {
523
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
524
        }
525
526
        if (!Normalizer::isNormalized($name)) {
527
            $name = Normalizer::normalize($name);
528
        }
529
530
        return $name;
531
    }
532
533
    /**
534
     * Get the name.
535
     *
536
     * @return string
537
     */
538
    public function getName(): string
539
    {
540
        return $this->name;
541
    }
542
543
    /**
544
     * Undelete.
545
     *
546
     * @param int    $conflict
547
     * @param string $recursion
548
     * @param bool   $recursion_first
549
     *
550
     * @return bool
551
     */
552
    public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool
553
    {
554
        if (!$this->_acl->isAllowed($this, 'w')) {
555
            throw new ForbiddenException(
556
                'not allowed to restore node '.$this->name,
557
                ForbiddenException::NOT_ALLOWED_TO_UNDELETE
558
            );
559
        }
560
        if (!$this->isDeleted()) {
561
            throw new Exception\Conflict(
562
                'node is not deleted, skip restore',
563
                Exception\Conflict::NOT_DELETED
564
            );
565
        }
566
567
        $parent = $this->getParent();
568
        if ($parent->isDeleted()) {
569
            throw new Exception\Conflict(
570
                'could not restore node '.$this->name.' into a deleted parent',
571
                Exception\Conflict::DELETED_PARENT
572
            );
573
        }
574
575
        if ($parent->childExists($this->name)) {
576
            if (NodeInterface::CONFLICT_MERGE === $conflict) {
577
                $this->copyTo($parent, $conflict);
578
                $this->delete(true);
579
            } elseif (NodeInterface::CONFLICT_RENAME === $conflict) {
580
                $this->setName($this->getDuplicateName());
581
                $this->raw_attributes['name'] = $this->name;
582
            } else {
583
                throw new Exception\Conflict(
584
                    'a node called '.$this->name.' does already exists in this collection',
585
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
586
                );
587
            }
588
        }
589
590
        if (null === $recursion) {
591
            $recursion_first = true;
592
            $recursion = uniqid();
593
        } else {
594
            $recursion_first = false;
595
        }
596
597
        $this->deleted = false;
598
599
        if ($this instanceof File) {
600
            $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...
601
            $new = $this->increaseVersion();
602
603
            $this->history[] = [
604
                'version' => $new,
605
                'changed' => $this->changed,
606
                'user' => $this->owner,
607
                'type' => File::HISTORY_UNDELETE,
608
                'storage' => $this->storage,
609
                'storage_adapter' => $this->storage_adapter,
610
                'size' => $this->size,
611
                'mime' => $this->mime,
612
            ];
613
614
            return $this->save([
615
                'name',
616
                'deleted',
617
                'history',
618
                'version',
619
            ], [], $recursion, $recursion_first);
620
        }
621
622
        $this->save([
623
                'name',
624
                'deleted',
625
            ], [], $recursion, $recursion_first);
626
627
        if ($this->isReference()) {
628
            return true;
629
        }
630
631
        return $this->doRecursiveAction(
632
                'undelete',
633
                [
634
                    'conflict' => $conflict,
635
                    'recursion' => $recursion,
636
                    'recursion_first' => false,
637
                ],
638
                NodeInterface::DELETED_ONLY
639
            );
640
    }
641
642
    /**
643
     * Is node deleted?
644
     *
645
     * @return bool
646
     */
647
    public function isDeleted(): bool
648
    {
649
        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...
650
    }
651
652
    /**
653
     * Get last modified timestamp.
654
     *
655
     * @return int
656
     */
657
    public function getLastModified(): int
658
    {
659
        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...
660
            return (int) $this->changed->toDateTime()->format('U');
661
        }
662
663
        return 0;
664
    }
665
666
    /**
667
     * Get unique id.
668
     *
669
     * @return ObjectId|string
670
     */
671
    public function getId(bool $string = false)
672
    {
673
        if (true === $string) {
674
            return (string) $this->_id;
675
        }
676
677
        return $this->_id;
678
    }
679
680
    /**
681
     * Get parent.
682
     *
683
     * @return Collection
684
     */
685
    public function getParent(): ?Collection
686
    {
687
        try {
688
            if ($this->isRoot()) {
689
                return null;
690
            }
691
            if ($this->isInRoot()) {
692
                return $this->_fs->getRoot();
693
            }
694
695
            $parent = $this->_fs->findNodeById($this->parent);
696
697
            if ($parent->isShare() && !$parent->isOwnerRequest() && null !== $this->_user) {
698
                $node = $this->_db->storage->findOne([
699
                        'owner' => $this->_user->getId(),
700
                        'shared' => true,
701
                        'reference' => $this->parent,
702
                    ]);
703
704
                return $this->_fs->initNode($node);
705
            }
706
707
            return $parent;
708
        } catch (Exception\NotFound $e) {
709
            throw new Exception\NotFound(
710
                'parent node '.$this->parent.' could not be found',
711
                Exception\NotFound::PARENT_NOT_FOUND
712
            );
713
        }
714
    }
715
716
    /**
717
     * Get parents.
718
     *
719
     * @param array $parents
720
     *
721
     * @return array
722
     */
723
    public function getParents(?NodeInterface $node = null, array $parents = []): array
724
    {
725
        if (null === $node) {
726
            $node = $this;
727
        }
728
729
        if ($node->isInRoot()) {
730
            return $parents;
731
        }
732
        $parent = $node->getParent();
733
        $parents[] = $parent;
734
735
        return $node->getParents($parent, $parents);
736
    }
737
738
    /**
739
     * Get as zip.
740
     */
741
    public function getZip(): void
742
    {
743
        $temp = $this->_server->getTempDir().DIRECTORY_SEPARATOR.'zip';
744
        if (!file_exists($temp)) {
745
            mkdir($temp, 0700, true);
746
        }
747
748
        ZipStream::$temp = $temp;
749
        $archive = new ZipStream($this->name.'.zip', 'application/zip', $this->name.'.zip');
750
        $this->zip($archive, false);
751
        $archive->finalize();
752
    }
753
754
    /**
755
     * Create zip.
756
     *
757
     * @param ZipStream     $archive
758
     * @param bool          $self    true means that the zip represents the collection itself instead a child of the zip
759
     * @param NodeInterface $parent
760
     * @param string        $path
761
     * @param int           $depth
762
     *
763
     * @return bool
764
     */
765
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
766
    {
767
        if (null === $parent) {
768
            $parent = $this;
769
        }
770
771
        if ($parent instanceof Collection) {
772
            $children = $parent->getChildNodes();
773
774
            if (true === $self && 0 === $depth) {
775
                $path = $parent->getName();
776
                $archive->addDirectory($path);
777
                $path .= DIRECTORY_SEPARATOR;
778
            } elseif (0 === $depth) {
779
                $path = '';
780
            } elseif (0 !== $depth) {
781
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
782
            }
783
784
            foreach ($children as $child) {
785
                $name = $path.$child->getName();
786
787
                if ($child instanceof Collection) {
788
                    $archive->addDirectory($name);
789
                    $this->zip($archive, $self, $child, $name, ++$depth);
790
                } elseif ($child instanceof File) {
791
                    try {
792
                        $archive->addFile($child->get(), $name);
793
                    } catch (\Exception $e) {
794
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
795
                            'category' => get_class($this),
796
                            'exception' => $e,
797
                        ]);
798
                    }
799
                }
800
            }
801
        } elseif ($parent instanceof File) {
802
            $archive->addFile($parent->get(), $parent->getName());
803
        }
804
805
        return true;
806
    }
807
808
    /**
809
     * Get mime type.
810
     *
811
     * @return string
812
     */
813
    public function getContentType(): string
814
    {
815
        return $this->mime;
816
    }
817
818
    /**
819
     * Is reference.
820
     *
821
     *  @return bool
822
     */
823
    public function isReference(): bool
824
    {
825
        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...
826
    }
827
828
    /**
829
     * Set app attributes.
830
     *
831
     * @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...
832
     * @param array        $attributes
833
     *
834
     * @return NodeInterface
835
     */
836
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
837
    {
838
        $this->app[$namespace] = $attributes;
839
        $this->save('app');
840
841
        return $this;
842
    }
843
844
    /**
845
     * Set app attribute.
846
     *
847
     * @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...
848
     * @param string       $attribute
849
     * @param mixed        $value
850
     *
851
     * @return NodeInterface
852
     */
853
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
854
    {
855
        if (!isset($this->app[$namespace])) {
856
            $this->app[$namespace] = [];
857
        }
858
859
        $this->app[$namespace][$attribute] = $value;
860
        $this->save('app');
861
862
        return $this;
863
    }
864
865
    /**
866
     * Remove app attribute.
867
     *
868
     * @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...
869
     *
870
     * @return NodeInterface
871
     */
872
    public function unsetAppAttributes(string $namespace): NodeInterface
873
    {
874
        if (isset($this->app[$namespace])) {
875
            unset($this->app[$namespace]);
876
            $this->save('app');
877
        }
878
879
        return $this;
880
    }
881
882
    /**
883
     * Remove app attribute.
884
     *
885
     * @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...
886
     * @param string       $attribute
887
     *
888
     * @return NodeInterface
889
     */
890
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
891
    {
892
        if (isset($this->app[$namespace][$attribute])) {
893
            unset($this->app[$namespace][$attribute]);
894
            $this->save('app');
895
        }
896
897
        return $this;
898
    }
899
900
    /**
901
     * Get app attribute.
902
     *
903
     * @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...
904
     * @param string       $attribute
905
     *
906
     * @return mixed
907
     */
908
    public function getAppAttribute(string $namespace, string $attribute)
909
    {
910
        if (isset($this->app[$namespace][$attribute])) {
911
            return $this->app[$namespace][$attribute];
912
        }
913
914
        return null;
915
    }
916
917
    /**
918
     * Get app attributes.
919
     *
920
     * @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...
921
     *
922
     * @return array
923
     */
924
    public function getAppAttributes(string $namespace): array
925
    {
926
        if (isset($this->app[$namespace])) {
927
            return $this->app[$namespace];
928
        }
929
930
        return [];
931
    }
932
933
    /**
934
     * Set meta attribute.
935
     *
936
     * @param   array|string
937
     * @param mixed $value
938
     * @param mixed $attributes
939
     *
940
     * @return NodeInterface
941
     */
942
    public function setMetaAttribute($attributes, $value = null): NodeInterface
943
    {
944
        $this->meta = self::validateMetaAttribute($attributes, $value, $this->meta);
945
        $this->save('meta');
946
947
        return $this;
948
    }
949
950
    /**
951
     * validate meta attribut.
952
     *
953
     * @param array|string $attributes
954
     * @param mixed        $value
955
     * @param array        $set
956
     *
957
     * @return array
958
     */
959
    public static function validateMetaAttribute($attributes, $value = null, array $set = []): array
960
    {
961
        if (is_string($attributes)) {
962
            $attributes = [
963
                $attributes => $value,
964
            ];
965
        }
966
967
        foreach ($attributes as $attribute => $value) {
968
            $const = __CLASS__.'::META_'.strtoupper($attribute);
969
            if (!defined($const)) {
970
                throw new Exception('meta attribute '.$attribute.' is not valid');
971
            }
972
973
            if (empty($value) && array_key_exists($attribute, $set)) {
974
                unset($set[$attribute]);
975
            } else {
976
                $set[$attribute] = $value;
977
            }
978
        }
979
980
        return $set;
981
    }
982
983
    /**
984
     * Get meta attributes as array.
985
     *
986
     * @param array|string $attribute Specify attributes to return
987
     *
988
     * @return array|string
989
     */
990
    public function getMetaAttribute($attribute = [])
991
    {
992
        if (is_string($attribute)) {
993
            if (isset($this->meta[$attribute])) {
994
                return $this->meta[$attribute];
995
            }
996
        } elseif (empty($attribute)) {
997
            return $this->meta;
998
        } elseif (is_array($attribute)) {
999
            return array_intersect_key($this->meta, array_flip($attribute));
1000
        }
1001
    }
1002
1003
    /**
1004
     * Mark node as readonly.
1005
     *
1006
     * @param bool $readonly
1007
     *
1008
     * @return bool
1009
     */
1010
    public function setReadonly(bool $readonly = true): bool
1011
    {
1012
        $this->readonly = $readonly;
1013
1014
        return $this->save('readonly');
1015
    }
1016
1017
    /**
1018
     * Mark node as self-destroyable.
1019
     *
1020
     * @param UTCDateTime $ts
1021
     *
1022
     * @return bool
1023
     */
1024
    public function setDestroyable(?UTCDateTime $ts): bool
1025
    {
1026
        $this->destroy = $ts;
1027
1028
        if (null === $ts) {
1029
            return $this->save([], 'destroy');
1030
        }
1031
1032
        return $this->save('destroy');
1033
    }
1034
1035
    /**
1036
     * Get original raw attributes before any processing.
1037
     *
1038
     * @return array
1039
     */
1040
    public function getRawAttributes(): array
1041
    {
1042
        return $this->raw_attributes;
1043
    }
1044
1045
    /**
1046
     * Check if node is in root.
1047
     *
1048
     * @return bool
1049
     */
1050
    public function isInRoot(): bool
1051
    {
1052
        return null === $this->parent;
1053
    }
1054
1055
    /**
1056
     * Check if node is an instance of the actual root collection.
1057
     *
1058
     * @return bool
1059
     */
1060
    public function isRoot(): bool
1061
    {
1062
        return null === $this->_id && ($this instanceof Collection);
1063
    }
1064
1065
    /**
1066
     * Resolve node path.
1067
     *
1068
     * @return string
1069
     */
1070
    public function getPath(): string
1071
    {
1072
        $path = '';
1073
        foreach (array_reverse($this->getParents()) as $parent) {
1074
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1075
        }
1076
1077
        $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...
1078
1079
        return $path;
1080
    }
1081
1082
    /**
1083
     * Save node attributes.
1084
     *
1085
     * @param array|string $attributes
1086
     * @param array|string $remove
1087
     * @param string       $recursion
1088
     * @param bool         $recursion_first
1089
     *
1090
     * @return bool
1091
     */
1092
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
1093
    {
1094
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
1095
            throw new ForbiddenException(
1096
                'not allowed to modify node '.$this->name,
1097
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
1098
            );
1099
        }
1100
1101
        if ($this instanceof Collection && $this->isRoot()) {
1102
            return false;
1103
        }
1104
1105
        $remove = (array) $remove;
1106
        $attributes = (array) $attributes;
1107
        $this->_hook->run(
1108
            'preSaveNodeAttributes',
1109
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
1110
        );
1111
1112
        try {
1113
            $set = [];
1114
1115
            foreach ($attributes as $attr) {
1116
                $set[$attr] = $this->{$attr};
1117
            }
1118
1119
            $update = [];
1120
            if (!empty($set)) {
1121
                $update['$set'] = $set;
1122
            }
1123
1124
            if (!empty($remove)) {
1125
                $remove = array_fill_keys($remove, 1);
1126
                $update['$unset'] = $remove;
1127
            }
1128
1129
            if (empty($update)) {
1130
                return false;
1131
            }
1132
            $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...
1133
                    '_id' => $this->_id,
1134
                ], $update);
1135
1136
            $this->_hook->run(
1137
                'postSaveNodeAttributes',
1138
                [$this, $attributes, $remove, $recursion, $recursion_first]
1139
            );
1140
1141
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1142
                'category' => get_class($this),
1143
                'params' => $update,
1144
            ]);
1145
1146
            return true;
1147
        } catch (\Exception $e) {
1148
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1149
                'category' => get_class($this),
1150
                'exception' => $e,
1151
            ]);
1152
1153
            throw $e;
1154
        }
1155
    }
1156
1157
    /**
1158
     * Duplicate name with a uniqid within name.
1159
     *
1160
     * @param string $name
1161
     *
1162
     * @return string
1163
     */
1164
    protected function getDuplicateName(?string $name = null): string
1165
    {
1166
        if (null === $name) {
1167
            $name = $this->name;
1168
        }
1169
1170
        if ($this instanceof Collection) {
1171
            return $name.' ('.substr(uniqid('', true), -4).')';
1172
        }
1173
        $ext = substr(strrchr($name, '.'), 1);
1174
1175
        if (false === $ext) {
1176
            return $name.' ('.substr(uniqid('', true), -4).')';
1177
        }
1178
        $name = substr($name, 0, -(strlen($ext) + 1));
1179
1180
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1181
    }
1182
1183
    /**
1184
     * Completly remove node.
1185
     *
1186
     * @return bool
1187
     */
1188
    abstract protected function _forceDelete(): bool;
1189
}
1190