Completed
Push — master ( b97427...e235cc )
by Raffael
30:35 queued 26:08
created

AbstractNode::setParent()   D

Complexity

Conditions 22
Paths 22

Size

Total Lines 82

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 506

Importance

Changes 0
Metric Value
dl 0
loc 82
ccs 0
cts 46
cp 0
rs 4.1666
c 0
b 0
f 0
cc 22
nc 22
nop 2
crap 506

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 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\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Hook;
19
use Balloon\Server;
20
use Balloon\Server\User;
21
use MimeType\MimeType;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use MongoDB\Database;
25
use Normalizer;
26
use Psr\Log\LoggerInterface;
27
use ZipStream\ZipStream;
28
29
abstract class AbstractNode implements NodeInterface
30
{
31
    /**
32
     * name max lenght.
33
     */
34
    const MAX_NAME_LENGTH = 255;
35
36
    /**
37
     * Unique id.
38
     *
39
     * @var ObjectId
40
     */
41
    protected $_id;
42
43
    /**
44
     * Node name.
45
     *
46
     * @var string
47
     */
48
    protected $name = '';
49
50
    /**
51
     * Owner.
52
     *
53
     * @var ObjectId
54
     */
55
    protected $owner;
56
57
    /**
58
     * Mime.
59
     *
60
     * @var string
61
     */
62
    protected $mime;
63
64
    /**
65
     * Meta attributes.
66
     *
67
     * @var array
68
     */
69
    protected $meta = [];
70
71
    /**
72
     * Parent collection.
73
     *
74
     * @var ObjectId
75
     */
76
    protected $parent;
77
78
    /**
79
     * Is file deleted.
80
     *
81
     * @var bool|UTCDateTime
82
     */
83
    protected $deleted = false;
84
85
    /**
86
     * Is shared?
87
     *
88
     * @var bool
89
     */
90
    protected $shared = false;
91
92
    /**
93
     * Destory at a certain time.
94
     *
95
     * @var UTCDateTime
96
     */
97
    protected $destroy;
98
99
    /**
100
     * Changed timestamp.
101
     *
102
     * @var UTCDateTime
103
     */
104
    protected $changed;
105
106
    /**
107
     * Created timestamp.
108
     *
109
     * @var UTCDateTime
110
     */
111
    protected $created;
112
113
    /**
114
     * Point to antother node (Means this node is reference to $reference).
115
     *
116
     * @var ObjectId
117
     */
118
    protected $reference;
119
120
    /**
121
     * Raw attributes before any processing or modifications.
122
     *
123
     * @var array
124
     */
125
    protected $raw_attributes;
126
127
    /**
128
     * Readonly flag.
129
     *
130
     * @var bool
131
     */
132
    protected $readonly = false;
133
134
    /**
135
     * App attributes.
136
     *
137
     * @var array
138
     */
139
    protected $app = [];
140
141
    /**
142
     * Filesystem.
143
     *
144
     * @var Filesystem
145
     */
146
    protected $_fs;
147
148
    /**
149
     * Database.
150
     *
151
     * @var Database
152
     */
153
    protected $_db;
154
155
    /**
156
     * User.
157
     *
158
     * @var User
159
     */
160
    protected $_user;
161
162
    /**
163
     * Logger.
164
     *
165
     * @var LoggerInterface
166
     */
167
    protected $_logger;
168
169
    /**
170
     * Server.
171
     *
172
     * @var Server
173
     */
174
    protected $_server;
175
176
    /**
177
     * Hook.
178
     *
179
     * @var Hook
180
     */
181
    protected $_hook;
182
183
    /**
184
     * Acl.
185
     *
186
     * @var Acl
187
     */
188
    protected $_acl;
189
190
    /**
191
     * Mount.
192
     *
193
     * @var ObjectId
194
     */
195
    protected $storage_reference;
196
197
    /**
198
     * Storage attributes.
199
     *
200
     * @var array
201
     */
202
    protected $storage;
203
204
    /**
205
     * File size for files, number of children for directories.
206
     *
207
     * @var int
208
     */
209
    protected $size = 0;
210
211
    /**
212
     * Acl.
213
     *
214
     * @var array
215
     */
216
    protected $acl = [];
217
218
    /**
219
     * Mount.
220
     *
221
     * @var array
222
     */
223
    protected $mount = [];
224
225
    /**
226
     * Lock.
227
     *
228
     * @var array
229
     */
230
    protected $lock;
231
232
    /**
233
     * Parent collection.
234
     *
235
     * @var Collection
236
     */
237
    protected $_parent;
238
239
    /**
240
     * Convert to filename.
241
     *
242
     * @return string
243
     */
244
    public function __toString()
245
    {
246
        return $this->name;
247
    }
248
249
    /**
250
     * Get owner.
251
     */
252
    public function getOwner(): ObjectId
253
    {
254
        return $this->owner;
255
    }
256
257
    /**
258
     * Set filesystem.
259
     */
260
    public function setFilesystem(Filesystem $fs): NodeInterface
261
    {
262
        $this->_fs = $fs;
263
        $this->_user = $fs->getUser();
264
265
        return $this;
266
    }
267
268
    /**
269
     * Get filesystem.
270
     */
271
    public function getFilesystem(): Filesystem
272
    {
273
        return $this->_fs;
274
    }
275
276
    /**
277
     * Check if $node is a sub node of any parent nodes of this node.
278
     */
279
    public function isSubNode(NodeInterface $node): bool
280
    {
281
        if ($node->getId() == $this->_id) {
282
            return true;
283
        }
284
285
        foreach ($node->getParents() as $node) {
286
            if ($node->getId() == $this->_id) {
287
                return true;
288
            }
289
        }
290
291
        if ($this->isRoot()) {
292
            return true;
293
        }
294
295
        return false;
296
    }
297
298
    /**
299
     * Move node.
300
     */
301
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
302
    {
303
        if ($this->parent == $parent->getId()) {
304
            throw new Exception\Conflict(
305
                'source node '.$this->name.' is already in the requested parent folder',
306
                Exception\Conflict::ALREADY_THERE
307
            );
308
        }
309
        if ($this->isSubNode($parent)) {
310
            throw new Exception\Conflict(
311
                'node called '.$this->name.' can not be moved into itself',
312
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
313
            );
314
        }
315
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
316
            throw new ForbiddenException(
317
                'not allowed to move node '.$this->name,
318
                ForbiddenException::NOT_ALLOWED_TO_MOVE
319
            );
320
        }
321
322
        $new_name = $parent->validateInsert($this->name, $conflict, get_class($this));
323
324
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
325
            throw new Exception\Conflict(
326
                'a shared folder can not be a child of a shared folder',
327
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
328
            );
329
        }
330
331
        if (NodeInterface::CONFLICT_RENAME === $conflict && $new_name !== $this->name) {
332
            $this->setName($new_name);
333
            $this->raw_attributes['name'] = $this->name;
334
        }
335
336
        if ($this instanceof Collection) {
337
            $query = [
338
                '$or' => [
339
                    ['reference' => ['exists' => true]],
340
                    ['shared' => true],
341
                ],
342
            ];
343
344
            if ($parent->isShared() && iterator_count($this->_fs->findNodesByFilterRecursive($this, $query, 0, 1)) !== 0) {
345
                throw new Exception\Conflict(
346
                    'folder contains a shared folder',
347
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
348
                );
349
            }
350
        }
351
352
        if ($this->isShared() && $parent->isSpecial()) {
353
            throw new Exception\Conflict(
354
                'a shared folder can not be an indirect child of a shared folder',
355
                Exception\Conflict::SHARED_NODE_CANT_BE_INDIRECT_CHILD_OF_SHARE
356
            );
357
        }
358
359
        if (($parent->isSpecial() && $this->shared != $parent->getShareId())
360
          || (!$parent->isSpecial() && $this->isShareMember())
361
          || ($parent->getMount() != $this->getParent()->getMount())) {
362
            $new = $this->copyTo($parent, $conflict);
363
            $this->delete();
364
365
            return $new;
366
        }
367
368
        if ($parent->childExists($this->name) && NodeInterface::CONFLICT_MERGE === $conflict) {
369
            $new = $this->copyTo($parent, $conflict);
370
            $this->delete(true);
371
372
            return $new;
373
        }
374
375
        $this->storage = $this->_parent->getStorage()->move($this, $parent);
376
        $this->parent = $parent->getRealId();
377
        $this->owner = $this->_user->getId();
378
379
        $this->save(['parent', 'shared', 'owner', 'storage']);
380
381
        return $this;
382
    }
383
384
    /**
385
     * Lock file.
386
     */
387
    public function lock(string $identifier, ?int $ttl = 1800): NodeInterface
388
    {
389
        if ($this->isLocked()) {
390
            if ($identifier !== $this->lock['id']) {
391
                throw new Exception\LockIdMissmatch('the unlock id must match the current lock id');
392
            }
393
        }
394
395
        $this->lock = $this->prepareLock($identifier, $ttl ?? 1800);
396
        $this->save(['lock']);
397
398
        return $this;
399
    }
400
401
    /**
402
     * Get lock.
403
     */
404
    public function getLock(): array
405
    {
406
        if (!$this->isLocked()) {
407
            throw new Exception\NotLocked('node is not locked');
408
        }
409
410
        return $this->lock;
411
    }
412
413
    /**
414
     * Is locked?
415
     */
416
    public function isLocked(): bool
417
    {
418
        if ($this->lock === null) {
419
            return false;
420
        }
421
        if ($this->lock['expire'] <= new UTCDateTime()) {
422
            return false;
423
        }
424
425
        return true;
426
    }
427
428
    /**
429
     * Unlock.
430
     */
431
    public function unlock(?string $identifier = null): NodeInterface
432
    {
433
        if (!$this->isLocked()) {
434
            throw new Exception\NotLocked('node is not locked');
435
        }
436
437
        if ($this->lock['owner'] != $this->_user->getId()) {
438
            throw new Exception\Forbidden('node is locked by another user');
439
        }
440
441
        if ($identifier !== null && $this->lock['id'] !== $identifier) {
442
            throw new Exception\LockIdMissmatch('the unlock id must match the current lock id');
443
        }
444
445
        $this->lock = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $lock.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
446
        $this->save(['lock']);
447
448
        return $this;
449
    }
450
451
    /**
452
     * Set node acl.
453
     */
454
    public function setAcl(array $acl): NodeInterface
455
    {
456
        if (!$this->_acl->isAllowed($this, 'm')) {
457
            throw new ForbiddenException(
458
                'not allowed to update acl',
459
                ForbiddenException::NOT_ALLOWED_TO_MANAGE
460
            );
461
        }
462
463
        if (!$this->isShareMember()) {
464
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
465
        }
466
467
        $this->_acl->validateAcl($this->_server, $acl);
468
        $this->acl = $acl;
469
        $this->save(['acl']);
470
471
        return $this;
472
    }
473
474
    /**
475
     * Get ACL.
476
     */
477
    public function getAcl(): array
478
    {
479
        if ($this->isReference()) {
480
            $acl = $this->_fs->findRawNode($this->getShareId())['acl'];
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId() targeting Balloon\Filesystem\Node\AbstractNode::getShareId() can also be of type boolean or null; however, Balloon\Filesystem::findRawNode() does only seem to accept object<MongoDB\BSON\ObjectId>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
481
        } else {
482
            $acl = $this->acl;
483
        }
484
485
        return $this->_acl->resolveAclTable($this->_server, $acl);
486
    }
487
488
    /**
489
     * Get share id.
490
     */
491
    public function getShareId(bool $reference = false): ?ObjectId
492
    {
493
        if ($this->isReference() && true === $reference) {
494
            return $this->_id;
495
        }
496
        if ($this->isShareMember() && true === $reference) {
497
            return $this->shared;
498
        }
499
        if ($this->isShared() && $this->isReference()) {
500
            return $this->reference;
501
        }
502
        if ($this->isShared()) {
503
            return $this->_id;
504
        }
505
        if ($this->isShareMember()) {
506
            return $this->shared;
507
        }
508
509
        return null;
510
    }
511
512
    /**
513
     * Get reference.
514
     */
515
    public function getReference(): ?ObjectId
516
    {
517
        return $this->reference;
518
    }
519
520
    /**
521
     * Get share node.
522
     */
523
    public function getShareNode(): ?Collection
524
    {
525
        if ($this->isShare()) {
526
            return $this;
527
        }
528
529
        if ($this->isSpecial()) {
530
            return $this->_fs->findNodeById($this->getShareId(true));
531
        }
532
533
        return null;
534
    }
535
536
    /**
537
     * Is node marked as readonly?
538
     */
539
    public function isReadonly(): bool
540
    {
541
        return $this->readonly;
542
    }
543
544
    /**
545
     * May write.
546
     */
547
    public function mayWrite(): bool
548
    {
549
        return Acl::PRIVILEGES_WEIGHT[$this->_acl->getAclPrivilege($this)] > Acl::PRIVILEGE_READ;
550
    }
551
552
    /**
553
     * Request is from node owner?
554
     */
555
    public function isOwnerRequest(): bool
556
    {
557
        return null !== $this->_user && $this->owner == $this->_user->getId();
558
    }
559
560
    /**
561
     * Check if node is kind of special.
562
     */
563
    public function isSpecial(): bool
564
    {
565
        if ($this->isShared()) {
566
            return true;
567
        }
568
        if ($this->isReference()) {
569
            return true;
570
        }
571
        if ($this->isShareMember()) {
572
            return true;
573
        }
574
575
        return false;
576
    }
577
578
    /**
579
     * Check if node is a sub node of a share.
580
     */
581
    public function isShareMember(): bool
582
    {
583
        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...
584
    }
585
586
    /**
587
     * Check if node is a sub node of an external storage mount.
588
     */
589
    public function isMountMember(): bool
590
    {
591
        return $this->storage_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...
592
    }
593
594
    /**
595
     * Is share.
596
     */
597
    public function isShare(): bool
598
    {
599
        return true === $this->shared && !$this->isReference();
600
    }
601
602
    /**
603
     * Is share (Reference or master share).
604
     */
605
    public function isShared(): bool
606
    {
607
        if (true === $this->shared) {
608
            return true;
609
        }
610
611
        return false;
612
    }
613
614
    /**
615
     * Set the name.
616
     */
617
    public function setName($name): bool
618
    {
619
        $name = $this->checkName($name);
620
621
        try {
622
            $child = $this->getParent()->getChild($name);
623
            if ($child->getId() != $this->_id) {
624
                throw new Exception\Conflict(
625
                    'a node called '.$name.' does already exists in this collection',
626
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
627
                );
628
            }
629
        } catch (Exception\NotFound $e) {
630
            //child does not exists, we can safely rename
631
        }
632
633
        $this->storage = $this->_parent->getStorage()->rename($this, $name);
634
        $this->name = $name;
635
636
        if ($this instanceof File) {
637
            $this->mime = MimeType::getType($this->name);
638
        }
639
640
        return $this->save(['name', 'storage', 'mime']);
641
    }
642
643
    /**
644
     * Check name.
645
     */
646
    public function checkName(string $name): string
647
    {
648
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
649
            throw new Exception\InvalidArgument('name contains invalid characters');
650
        }
651
        if (strlen($name) > self::MAX_NAME_LENGTH) {
652
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
653
        }
654
655
        if (!Normalizer::isNormalized($name)) {
656
            $name = Normalizer::normalize($name);
657
        }
658
659
        return $name;
660
    }
661
662
    /**
663
     * Get the name.
664
     */
665
    public function getName(): string
666
    {
667
        return $this->name;
668
    }
669
670
    /**
671
     * Get mount node.
672
     */
673
    public function getMount(): ?ObjectId
674
    {
675
        return count($this->mount) > 0 ? $this->_id : $this->storage_reference;
676
    }
677
678
    /**
679
     * Undelete.
680
     */
681
    public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool
682
    {
683
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
684
            throw new ForbiddenException(
685
                'not allowed to restore node '.$this->name,
686
                ForbiddenException::NOT_ALLOWED_TO_UNDELETE
687
            );
688
        }
689
690
        $parent = $this->getParent();
691
        if ($parent->isDeleted()) {
692
            throw new Exception\Conflict(
693
                'could not restore node '.$this->name.' into a deleted parent',
694
                Exception\Conflict::DELETED_PARENT
695
            );
696
        }
697
698
        if ($parent->childExists($this->name)) {
699
            if (NodeInterface::CONFLICT_MERGE === $conflict) {
700
                $new = $this->copyTo($parent, $conflict, null, true, NodeInterface::DELETED_INCLUDE);
701
702
                if ($new->getId() != $this->getId()) {
703
                    $this->delete(true);
704
                }
705
            } elseif (NodeInterface::CONFLICT_RENAME === $conflict) {
706
                $this->setName($this->getDuplicateName());
707
                $this->raw_attributes['name'] = $this->name;
708
            } else {
709
                throw new Exception\Conflict(
710
                    'a node called '.$this->name.' does already exists in this collection',
711
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
712
                );
713
            }
714
        }
715
716
        if (null === $recursion) {
717
            $recursion_first = true;
718
            $recursion = uniqid();
719
        } else {
720
            $recursion_first = false;
721
        }
722
723
        $this->storage = $this->_parent->getStorage()->undelete($this);
724
        $this->deleted = false;
725
726
        $this->save([
727
                'storage',
728
                'name',
729
                'deleted',
730
            ], [], $recursion, $recursion_first);
731
732
        if ($this instanceof File || $this->isReference() || $this->isMounted() || $this->isFiltered()) {
733
            return true;
734
        }
735
736
        return $this->doRecursiveAction(function ($node) use ($conflict, $recursion) {
737
            $node->undelete($conflict, $recursion, false);
738
        }, NodeInterface::DELETED_ONLY);
739
    }
740
741
    /**
742
     * Is node deleted?
743
     */
744
    public function isDeleted(): bool
745
    {
746
        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...
747
    }
748
749
    /**
750
     * Get last modified timestamp.
751
     */
752
    public function getLastModified(): int
753
    {
754
        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...
755
            return (int) $this->changed->toDateTime()->format('U');
756
        }
757
758
        return 0;
759
    }
760
761
    /**
762
     * Get unique id.
763
     */
764 1
    public function getId(): ?ObjectId
765
    {
766 1
        return $this->_id;
767
    }
768
769
    /**
770
     * Get parent.
771
     */
772
    public function getParent(): ?Collection
773
    {
774
        return $this->_parent;
775
    }
776
777
    /**
778
     * Get parents.
779
     */
780
    public function getParents(?NodeInterface $node = null, array $parents = []): array
781
    {
782
        if (null === $node) {
783
            $node = $this;
784
        }
785
786
        if ($node->isInRoot()) {
787
            return $parents;
788
        }
789
        $parent = $node->getParent();
790
        $parents[] = $parent;
791
792
        return $node->getParents($parent, $parents);
793
    }
794
795
    /**
796
     * Get as zip.
797
     */
798
    public function getZip(): void
799
    {
800
        $archive = new ZipStream($this->name.'.zip');
801
        $this->zip($archive, false);
802
        $archive->finish();
803
    }
804
805
    /**
806
     * Create zip.
807
     */
808
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
809
    {
810
        if (null === $parent) {
811
            $parent = $this;
812
        }
813
814
        if ($parent instanceof Collection) {
815
            $children = $parent->getChildNodes();
816
817
            if (true === $self && 0 === $depth) {
818
                $path = $parent->getName().DIRECTORY_SEPARATOR;
0 ignored issues
show
Bug introduced by
Consider using $parent->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
819
            } elseif (0 === $depth) {
820
                $path = '';
821
            } elseif (0 !== $depth) {
822
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
0 ignored issues
show
Bug introduced by
Consider using $parent->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
823
            }
824
825
            foreach ($children as $child) {
826
                $name = $path.$child->getName();
827
828
                if ($child instanceof Collection) {
829
                    $this->zip($archive, $self, $child, $name, ++$depth);
830
                } elseif ($child instanceof File) {
831
                    try {
832
                        $resource = $child->get();
833
                        if ($resource !== null) {
834
                            $archive->addFileFromStream($name, $resource);
835
                        }
836
                    } catch (\Exception $e) {
837
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
838
                            'category' => get_class($this),
839
                            'exception' => $e,
840
                        ]);
841
                    }
842
                }
843
            }
844
        } elseif ($parent instanceof File) {
845
            $resource = $parent->get();
846
            if ($resource !== null) {
847
                $archive->addFileFromStream($parent->getName(), $resource);
0 ignored issues
show
Bug introduced by
Consider using $parent->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
848
            }
849
        }
850
851
        return true;
852
    }
853
854
    /**
855
     * Get mime type.
856
     */
857 1
    public function getContentType(): string
858
    {
859 1
        return $this->mime;
860
    }
861
862
    /**
863
     * Is reference.
864
     */
865
    public function isReference(): bool
866
    {
867
        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...
868
    }
869
870
    /**
871
     * Set app attributes.
872
     */
873
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
874
    {
875
        $this->app[$namespace] = $attributes;
876
        $this->save('app.'.$namespace);
877
878
        return $this;
879
    }
880
881
    /**
882
     * Set app attribute.
883
     */
884
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
885
    {
886
        if (!isset($this->app[$namespace])) {
887
            $this->app[$namespace] = [];
888
        }
889
890
        $this->app[$namespace][$attribute] = $value;
891
        $this->save('app.'.$namespace);
892
893
        return $this;
894
    }
895
896
    /**
897
     * Remove app attribute.
898
     */
899
    public function unsetAppAttributes(string $namespace): NodeInterface
900
    {
901
        if (isset($this->app[$namespace])) {
902
            unset($this->app[$namespace]);
903
            $this->save('app.'.$namespace);
904
        }
905
906
        return $this;
907
    }
908
909
    /**
910
     * Remove app attribute.
911
     */
912
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
913
    {
914
        if (isset($this->app[$namespace][$attribute])) {
915
            unset($this->app[$namespace][$attribute]);
916
            $this->save('app'.$namespace);
917
        }
918
919
        return $this;
920
    }
921
922
    /**
923
     * Get app attribute.
924
     */
925
    public function getAppAttribute(string $namespace, string $attribute)
926
    {
927
        if (isset($this->app[$namespace][$attribute])) {
928
            return $this->app[$namespace][$attribute];
929
        }
930
931
        return null;
932
    }
933
934
    /**
935
     * Get app attributes.
936
     */
937
    public function getAppAttributes(string $namespace): array
938
    {
939
        if (isset($this->app[$namespace])) {
940
            return $this->app[$namespace];
941
        }
942
943
        return [];
944
    }
945
946
    /**
947
     * Set meta attributes.
948
     */
949
    public function setMetaAttributes(array $attributes): NodeInterface
950
    {
951
        $attributes = $this->validateMetaAttributes($attributes);
952
        foreach ($attributes as $attribute => $value) {
953
            if (empty($value) && isset($this->meta[$attribute])) {
954
                unset($this->meta[$attribute]);
955
            } elseif (!empty($value)) {
956
                $this->meta[$attribute] = $value;
957
            }
958
        }
959
960
        $this->save('meta');
961
962
        return $this;
963
    }
964
965
    /**
966
     * Get meta attributes as array.
967
     */
968
    public function getMetaAttributes(array $attributes = []): array
969
    {
970
        if (empty($attributes)) {
971
            return $this->meta;
972
        }
973
        if (is_array($attributes)) {
974
            return array_intersect_key($this->meta, array_flip($attributes));
975
        }
976
    }
977
978
    /**
979
     * Mark node as readonly.
980
     */
981
    public function setReadonly(bool $readonly = true): bool
982
    {
983
        $this->readonly = $readonly;
984
        $this->storage = $this->_parent->getStorage()->readonly($this, $readonly);
985
986
        return $this->save(['readonly', 'storage']);
987
    }
988
989
    /**
990
     * Mark node as self-destroyable.
991
     */
992
    public function setDestroyable(?UTCDateTime $ts): bool
993
    {
994
        $this->destroy = $ts;
995
996
        if (null === $ts) {
997
            return $this->save([], 'destroy');
998
        }
999
1000
        return $this->save('destroy');
1001
    }
1002
1003
    /**
1004
     * Get original raw attributes before any processing.
1005
     */
1006
    public function getRawAttributes(): array
1007
    {
1008
        return $this->raw_attributes;
1009
    }
1010
1011
    /**
1012
     * Check if node is in root.
1013
     */
1014
    public function isInRoot(): bool
1015
    {
1016
        return null === $this->parent;
1017
    }
1018
1019
    /**
1020
     * Check if node is an instance of the actual root collection.
1021
     */
1022
    public function isRoot(): bool
1023
    {
1024
        return null === $this->_id && ($this instanceof Collection);
1025
    }
1026
1027
    /**
1028
     * Resolve node path.
1029
     */
1030
    public function getPath(): string
1031
    {
1032
        $path = '';
1033
        foreach (array_reverse($this->getParents()) as $parent) {
1034
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1035
        }
1036
1037
        $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...
1038
1039
        return $path;
1040
    }
1041
1042
    /**
1043
     * Save node attributes.
1044
     *
1045
     * @param array|string $attributes
1046
     * @param array|string $remove
1047
     * @param string       $recursion
1048
     */
1049
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
1050
    {
1051
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
1052
            throw new ForbiddenException(
1053
                'not allowed to modify node '.$this->name,
1054
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
1055
            );
1056
        }
1057
1058
        if ($this instanceof Collection && $this->isRoot()) {
1059
            return false;
1060
        }
1061
1062
        $remove = (array) $remove;
1063
        $attributes = (array) $attributes;
1064
        $this->_hook->run(
1065
            'preSaveNodeAttributes',
1066
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
1067
        );
1068
1069
        try {
1070
            $set = [];
1071
            $values = $this->getAttributes();
1072
            foreach ($attributes as $attr) {
1073
                $set[$attr] = $this->getArrayValue($values, $attr);
1074
            }
1075
1076
            $update = [];
1077
            if (!empty($set)) {
1078
                $update['$set'] = $set;
1079
            }
1080
1081
            if (!empty($remove)) {
1082
                $remove = array_fill_keys($remove, 1);
1083
                $update['$unset'] = $remove;
1084
            }
1085
1086
            if (empty($update)) {
1087
                return false;
1088
            }
1089
            $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...
1090
                    '_id' => $this->_id,
1091
                ], $update);
1092
1093
            $this->_hook->run(
1094
                'postSaveNodeAttributes',
1095
                [$this, $attributes, $remove, $recursion, $recursion_first]
1096
            );
1097
1098
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1099
                'category' => get_class($this),
1100
                'params' => $update,
1101
            ]);
1102
1103
            return true;
1104
        } catch (\Exception $e) {
1105
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1106
                'category' => get_class($this),
1107
                'exception' => $e,
1108
            ]);
1109
1110
            throw $e;
1111
        }
1112
    }
1113
1114
    /**
1115
     * Duplicate name with a uniqid within name.
1116
     */
1117
    public function getDuplicateName(?string $name = null, ?string $class = null): string
1118
    {
1119
        if (null === $name) {
1120
            $name = $this->name;
1121
        }
1122
1123
        if (null === $class) {
1124
            $class = get_class($this);
1125
        }
1126
1127
        if ($class === Collection::class) {
1128
            return $name.' ('.substr(uniqid('', true), -4).')';
1129
        }
1130
1131
        $ext = substr(strrchr($name, '.'), 1);
1132
        if (false === $ext) {
1133
            return $name.' ('.substr(uniqid('', true), -4).')';
1134
        }
1135
1136
        $name = substr($name, 0, -(strlen($ext) + 1));
1137
1138
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1139
    }
1140
1141
    /**
1142
     * Prepare lock.
1143
     */
1144
    protected function prepareLock(string $identifier, int $ttl = 1800): array
1145
    {
1146
        return [
1147
             'owner' => $this->_user->getId(),
1148
            'created' => new UTCDateTime(),
1149
            'id' => $identifier,
1150
            'expire' => new UTCDateTime((time() + $ttl) * 1000),
1151
        ];
1152
    }
1153
1154
    /**
1155
     * Get array value via string path.
1156
     */
1157
    protected function getArrayValue(iterable $array, string $path, string $separator = '.')
1158
    {
1159
        if (isset($array[$path])) {
1160
            return $array[$path];
1161
        }
1162
        $keys = explode($separator, $path);
1163
1164
        foreach ($keys as $key) {
1165
            if (!array_key_exists($key, $array)) {
1166
                return;
1167
            }
1168
1169
            $array = $array[$key];
1170
        }
1171
1172
        return $array;
1173
    }
1174
1175
    /**
1176
     * Validate meta attributes.
1177
     */
1178
    protected function validateMetaAttributes(array $attributes): array
1179
    {
1180
        foreach ($attributes as $attribute => $value) {
1181
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1182
            if (!defined($const)) {
1183
                throw new Exception('meta attribute '.$attribute.' is not valid');
1184
            }
1185
1186
            if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) {
1187
                throw new Exception('tags meta attribute must be an array of strings');
1188
            }
1189
1190
            if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) {
1191
                throw new Exception($attribute.' meta attribute must be a string');
1192
            }
1193
        }
1194
1195
        return $attributes;
1196
    }
1197
1198
    /**
1199
     * Completly remove node.
1200
     */
1201
    abstract protected function _forceDelete(): bool;
1202
}
1203