Test Failed
Push — master ( bca3c6...21ca38 )
by Raffael
09:03
created

AbstractNode::getReference()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
     * Acl.
206
     *
207
     * @var array
208
     */
209
    protected $acl = [];
210
211
    /**
212
     * Mount.
213
     *
214
     * @var array
215
     */
216
    protected $mount = [];
217
218
    /**
219
     * Parent collection.
220
     *
221
     * @var Collection
222
     */
223
    protected $_parent;
224
225
    /**
226
     * Convert to filename.
227
     *
228
     * @return string
229
     */
230
    public function __toString()
231
    {
232
        return $this->name;
233
    }
234
235
    /**
236
     * Get owner.
237
     */
238
    public function getOwner(): ObjectId
239
    {
240
        return $this->owner;
241
    }
242
243
    /**
244
     * Set filesystem.
245
     */
246
    public function setFilesystem(Filesystem $fs): NodeInterface
247
    {
248
        $this->_fs = $fs;
249
        $this->_user = $fs->getUser();
250
251
        return $this;
252
    }
253
254
    /**
255
     * Get filesystem.
256
     */
257
    public function getFilesystem(): Filesystem
258
    {
259
        return $this->_fs;
260
    }
261
262
    /**
263
     * Check if $node is a sub node of any parent nodes of this node.
264
     */
265
    public function isSubNode(NodeInterface $node): bool
266
    {
267
        if ($node->getId() == $this->_id) {
268
            return true;
269
        }
270
271
        foreach ($node->getParents() as $node) {
272
            if ($node->getId() == $this->_id) {
273
                return true;
274
            }
275
        }
276
277
        if ($this->isRoot()) {
278
            return true;
279
        }
280
281
        return false;
282
    }
283
284
    /**
285
     * Move node.
286
     */
287
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
288
    {
289
        if ($this->parent == $parent->getId()) {
290
            throw new Exception\Conflict(
291
                'source node '.$this->name.' is already in the requested parent folder',
292
                Exception\Conflict::ALREADY_THERE
293
            );
294
        }
295
        if ($this->isSubNode($parent)) {
296
            throw new Exception\Conflict(
297
                'node called '.$this->name.' can not be moved into itself',
298
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
299
            );
300
        }
301
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
302
            throw new ForbiddenException(
303
                'not allowed to move node '.$this->name,
304
                ForbiddenException::NOT_ALLOWED_TO_MOVE
305
            );
306
        }
307
308
        $exists = $parent->childExists($this->name);
309
        if (true === $exists && NodeInterface::CONFLICT_NOACTION === $conflict) {
310
            throw new Exception\Conflict(
311
                'a node called '.$this->name.' does already exists in this collection',
312
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
313
            );
314
        }
315
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
316
            throw new Exception\Conflict(
317
                'a shared folder can not be a child of a shared folder',
318
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
319
            );
320
        }
321
        if ($parent->isDeleted()) {
322
            throw new Exception\Conflict(
323
                'cannot move node into a deleted collction',
324
                Exception\Conflict::DELETED_PARENT
325
            );
326
        }
327
328
        if (true === $exists && NodeInterface::CONFLICT_RENAME === $conflict) {
329
            $this->setName($this->getDuplicateName());
330
            $this->raw_attributes['name'] = $this->name;
331
        }
332
333
        if ($parent->isFiltered()) {
334
            throw new Exception\Conflict(
335
                'can not move node into a filtered parent collection',
336
                Exception\Conflict::DYNAMIC_PARENT
337
            );
338
        }
339
340
        if ($this instanceof Collection) {
341
            $query = [
342
                '$or' => [
343
                    ['reference' => ['exists' => true]],
344
                    ['shared' => true],
345
                ],
346
            ];
347
348
            if ($parent->isShared() && iterator_count($this->_fs->findNodesByFilterRecursive($this, $query, 0, 1)) !== 0) {
349
                throw new Exception\Conflict(
350
                    'folder contains a shared folder',
351
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
352
                );
353
            }
354
        }
355
356
        if ($this->isShared() && $parent->isSpecial()) {
357
            throw new Exception\Conflict(
358
                'a shared folder can not be an indirect child of a shared folder',
359
                Exception\Conflict::SHARED_NODE_CANT_BE_INDIRECT_CHILD_OF_SHARE
360
            );
361
        }
362
363
        if (($parent->isSpecial() && $this->shared != $parent->getShareId())
364
          || (!$parent->isSpecial() && $this->isShareMember())
365
          || ($parent->getMount() != $this->getParent()->getMount())) {
366
            $new = $this->copyTo($parent, $conflict);
367
            $this->delete();
368
369
            return $new;
370
        }
371
372
        if (true === $exists && NodeInterface::CONFLICT_MERGE === $conflict) {
373
            $new = $this->copyTo($parent, $conflict);
374
            $this->delete(true);
375
376
            return $new;
377
        }
378
379
        $this->storage = $this->_parent->getStorage()->move($this, $parent);
380
        $this->parent = $parent->getRealId();
381
        $this->owner = $this->_user->getId();
382
383
        $this->save(['parent', 'shared', 'owner', 'storage']);
384
385
        return $this;
386
    }
387
388
    /**
389
     * Set node acl.
390
     */
391
    public function setAcl(array $acl): NodeInterface
392
    {
393
        if (!$this->_acl->isAllowed($this, 'm')) {
394
            throw new ForbiddenException(
395
                'not allowed to update acl',
396
                ForbiddenException::NOT_ALLOWED_TO_MANAGE
397
            );
398
        }
399
400
        if (!$this->isShareMember()) {
401
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
402
        }
403
404
        $this->_acl->validateAcl($this->_server, $acl);
405
        $this->acl = $acl;
406
        $this->save(['acl']);
407
408
        return $this;
409
    }
410
411
    /**
412
     * Get ACL.
413
     */
414
    public function getAcl(): array
415
    {
416
        if ($this->isReference()) {
417
            $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...
418
        } else {
419
            $acl = $this->acl;
420
        }
421
422
        return $this->_acl->resolveAclTable($this->_server, $acl);
423
    }
424
425
    /**
426
     * Get share id.
427
     */
428
    public function getShareId(bool $reference = false): ?ObjectId
429
    {
430
        if ($this->isReference() && true === $reference) {
431
            return $this->_id;
432
        }
433
        if ($this->isShareMember() && true === $reference) {
434
            return $this->shared;
435
        }
436
        if ($this->isShared() && $this->isReference()) {
437
            return $this->reference;
438
        }
439
        if ($this->isShared()) {
440
            return $this->_id;
441
        }
442
        if ($this->isShareMember()) {
443
            return $this->shared;
444
        }
445
446
        return null;
447
    }
448
449
    /**
450
     * Get reference.
451
     */
452
    public function getReference(): ?ObjectId
453
    {
454
        return $this->reference;
455
    }
456
457
    /**
458
     * Get share node.
459
     */
460
    public function getShareNode(): ?Collection
461
    {
462
        if ($this->isShare()) {
463
            return $this;
464
        }
465
466
        if ($this->isSpecial()) {
467
            return $this->_fs->findNodeById($this->getShareId(true));
468
        }
469
470
        return null;
471
    }
472
473
    /**
474
     * Is node marked as readonly?
475
     */
476
    public function isReadonly(): bool
477
    {
478
        return $this->readonly;
479
    }
480
481
    /**
482
     * Request is from node owner?
483
     */
484
    public function isOwnerRequest(): bool
485
    {
486
        return null !== $this->_user && $this->owner == $this->_user->getId();
487
    }
488
489
    /**
490
     * Check if node is kind of special.
491
     */
492
    public function isSpecial(): bool
493
    {
494
        if ($this->isShared()) {
495
            return true;
496
        }
497
        if ($this->isReference()) {
498
            return true;
499
        }
500
        if ($this->isShareMember()) {
501
            return true;
502
        }
503
504
        return false;
505
    }
506
507
    /**
508
     * Check if node is a sub node of a share.
509
     */
510
    public function isShareMember(): bool
511
    {
512
        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...
513
    }
514
515
    /**
516
     * Check if node is a sub node of an external storage mount.
517
     */
518
    public function isMountMember(): bool
519
    {
520
        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...
521
    }
522
523
    /**
524
     * Is share.
525
     */
526
    public function isShare(): bool
527
    {
528
        return true === $this->shared && !$this->isReference();
529
    }
530
531
    /**
532
     * Is share (Reference or master share).
533
     */
534
    public function isShared(): bool
535
    {
536
        if (true === $this->shared) {
537
            return true;
538
        }
539
540
        return false;
541
    }
542
543
    /**
544
     * Set the name.
545
     */
546
    public function setName($name): bool
547
    {
548
        $name = $this->checkName($name);
549
550
        try {
551
            $child = $this->getParent()->getChild($name);
552
            if ($child->getId() != $this->_id) {
553
                throw new Exception\Conflict(
554
                    'a node called '.$name.' does already exists in this collection',
555
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
556
                );
557
            }
558
        } catch (Exception\NotFound $e) {
559
            //child does not exists, we can safely rename
560
        }
561
562
        $this->storage = $this->_parent->getStorage()->rename($this, $name);
563
        $this->name = $name;
564
565
        if ($this instanceof File) {
566
            $this->mime = MimeType::getType($this->name);
567
        }
568
569
        return $this->save(['name', 'storage', 'mime']);
570
    }
571
572
    /**
573
     * Check name.
574
     */
575
    public function checkName(string $name): string
576
    {
577
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
578
            throw new Exception\InvalidArgument('name contains invalid characters');
579
        }
580
        if (strlen($name) > self::MAX_NAME_LENGTH) {
581
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
582
        }
583
584
        if (!Normalizer::isNormalized($name)) {
585
            $name = Normalizer::normalize($name);
586
        }
587
588
        return $name;
589
    }
590
591
    /**
592
     * Get the name.
593
     */
594
    public function getName(): string
595
    {
596
        return $this->name;
597
    }
598
599
    /**
600
     * Get mount node.
601
     */
602
    public function getMount(): ?ObjectId
603
    {
604
        return count($this->mount) > 0 ? $this->_id : $this->storage_reference;
605
    }
606
607
    /**
608
     * Undelete.
609
     */
610
    public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool
611
    {
612
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
613
            throw new ForbiddenException(
614
                'not allowed to restore node '.$this->name,
615
                ForbiddenException::NOT_ALLOWED_TO_UNDELETE
616
            );
617
        }
618
619
        $parent = $this->getParent();
620
        if ($parent->isDeleted()) {
621
            throw new Exception\Conflict(
622
                'could not restore node '.$this->name.' into a deleted parent',
623
                Exception\Conflict::DELETED_PARENT
624
            );
625
        }
626
627
        if ($parent->childExists($this->name)) {
628
            if (NodeInterface::CONFLICT_MERGE === $conflict) {
629
                $new = $this->copyTo($parent, $conflict, null, true, NodeInterface::DELETED_INCLUDE);
630
631
                if ($new->getId() != $this->getId()) {
632
                    $this->delete(true);
633
                }
634
            } elseif (NodeInterface::CONFLICT_RENAME === $conflict) {
635
                $this->setName($this->getDuplicateName());
636
                $this->raw_attributes['name'] = $this->name;
637
            } else {
638
                throw new Exception\Conflict(
639
                    'a node called '.$this->name.' does already exists in this collection',
640
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
641
                );
642
            }
643
        }
644
645
        if (null === $recursion) {
646
            $recursion_first = true;
647
            $recursion = uniqid();
648
        } else {
649
            $recursion_first = false;
650
        }
651
652
        $this->storage = $this->_parent->getStorage()->undelete($this);
653
        $this->deleted = false;
654
655
        $this->save([
656
                'storage',
657
                'name',
658
                'deleted',
659
            ], [], $recursion, $recursion_first);
660
661
        if ($this instanceof File || $this->isReference() || $this->isMounted() || $this->isFiltered()) {
662
            return true;
663
        }
664
665
        return $this->doRecursiveAction(function ($node) use ($conflict, $recursion) {
666
            $node->undelete($conflict, $recursion, false);
667
        }, NodeInterface::DELETED_ONLY);
668
    }
669
670
    /**
671
     * Is node deleted?
672
     */
673
    public function isDeleted(): bool
674
    {
675
        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...
676
    }
677
678
    /**
679
     * Get last modified timestamp.
680
     */
681
    public function getLastModified(): int
682
    {
683
        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...
684
            return (int) $this->changed->toDateTime()->format('U');
685
        }
686
687
        return 0;
688
    }
689
690
    /**
691
     * Get unique id.
692
     */
693 1
    public function getId(): ?ObjectId
694
    {
695 1
        return $this->_id;
696
    }
697
698
    /**
699
     * Get parent.
700
     */
701
    public function getParent(): ?Collection
702
    {
703
        return $this->_parent;
704
    }
705
706
    /**
707
     * Get parents.
708
     */
709
    public function getParents(?NodeInterface $node = null, array $parents = []): array
710
    {
711
        if (null === $node) {
712
            $node = $this;
713
        }
714
715
        if ($node->isInRoot()) {
716
            return $parents;
717
        }
718
        $parent = $node->getParent();
719
        $parents[] = $parent;
720
721
        return $node->getParents($parent, $parents);
722
    }
723
724
    /**
725
     * Get as zip.
726
     */
727
    public function getZip(): void
728
    {
729
        $archive = new ZipStream($this->name.'.zip');
730
        $this->zip($archive, false);
731
        $archive->finish();
732
    }
733
734
    /**
735
     * Create zip.
736
     */
737
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
738
    {
739
        if (null === $parent) {
740
            $parent = $this;
741
        }
742
743
        if ($parent instanceof Collection) {
744
            $children = $parent->getChildNodes();
745
746
            if (true === $self && 0 === $depth) {
747
                $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...
748
            } elseif (0 === $depth) {
749
                $path = '';
750
            } elseif (0 !== $depth) {
751
                $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...
752
            }
753
754
            foreach ($children as $child) {
755
                $name = $path.$child->getName();
756
757
                if ($child instanceof Collection) {
758
                    $this->zip($archive, $self, $child, $name, ++$depth);
759
                } elseif ($child instanceof File) {
760
                    try {
761
                        $resource = $child->get();
762
                        if ($resource !== null) {
763
                            $archive->addFileFromStream($name, $resource);
764
                        }
765
                    } catch (\Exception $e) {
766
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
767
                            'category' => get_class($this),
768
                            'exception' => $e,
769
                        ]);
770
                    }
771
                }
772
            }
773
        } elseif ($parent instanceof File) {
774
            $resource = $parent->get();
775
            if ($resource !== null) {
776
                $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...
777
            }
778
        }
779
780
        return true;
781
    }
782
783
    /**
784
     * Get mime type.
785
     */
786 1
    public function getContentType(): string
787
    {
788 1
        return $this->mime;
789
    }
790
791
    /**
792
     * Is reference.
793
     */
794
    public function isReference(): bool
795
    {
796
        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...
797
    }
798
799
    /**
800
     * Set app attributes.
801
     */
802
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
803
    {
804
        $this->app[$namespace] = $attributes;
805
        $this->save('app.'.$namespace);
806
807
        return $this;
808
    }
809
810
    /**
811
     * Set app attribute.
812
     */
813
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
814
    {
815
        if (!isset($this->app[$namespace])) {
816
            $this->app[$namespace] = [];
817
        }
818
819
        $this->app[$namespace][$attribute] = $value;
820
        $this->save('app.'.$namespace);
821
822
        return $this;
823
    }
824
825
    /**
826
     * Remove app attribute.
827
     */
828
    public function unsetAppAttributes(string $namespace): NodeInterface
829
    {
830
        if (isset($this->app[$namespace])) {
831
            unset($this->app[$namespace]);
832
            $this->save('app.'.$namespace);
833
        }
834
835
        return $this;
836
    }
837
838
    /**
839
     * Remove app attribute.
840
     */
841
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
842
    {
843
        if (isset($this->app[$namespace][$attribute])) {
844
            unset($this->app[$namespace][$attribute]);
845
            $this->save('app'.$namespace);
846
        }
847
848
        return $this;
849
    }
850
851
    /**
852
     * Get app attribute.
853
     */
854
    public function getAppAttribute(string $namespace, string $attribute)
855
    {
856
        if (isset($this->app[$namespace][$attribute])) {
857
            return $this->app[$namespace][$attribute];
858
        }
859
860
        return null;
861
    }
862
863
    /**
864
     * Get app attributes.
865
     */
866
    public function getAppAttributes(string $namespace): array
867
    {
868
        if (isset($this->app[$namespace])) {
869
            return $this->app[$namespace];
870
        }
871
872
        return [];
873
    }
874
875
    /**
876
     * Set meta attributes.
877
     */
878
    public function setMetaAttributes(array $attributes): NodeInterface
879
    {
880
        $attributes = $this->validateMetaAttributes($attributes);
881
        foreach ($attributes as $attribute => $value) {
882
            if (empty($value) && isset($this->meta[$attribute])) {
883
                unset($this->meta[$attribute]);
884
            } elseif (!empty($value)) {
885
                $this->meta[$attribute] = $value;
886
            }
887
        }
888
889
        $this->save('meta');
890
891
        return $this;
892
    }
893
894
    /**
895
     * Get meta attributes as array.
896
     */
897
    public function getMetaAttributes(array $attributes = []): array
898
    {
899
        if (empty($attributes)) {
900
            return $this->meta;
901
        }
902
        if (is_array($attributes)) {
903
            return array_intersect_key($this->meta, array_flip($attributes));
904
        }
905
    }
906
907
    /**
908
     * Mark node as readonly.
909
     */
910
    public function setReadonly(bool $readonly = true): bool
911
    {
912
        $this->readonly = $readonly;
913
        $this->storage = $this->_parent->getStorage()->readonly($this, $readonly);
914
915
        return $this->save(['readonly', 'storage']);
916
    }
917
918
    /**
919
     * Mark node as self-destroyable.
920
     */
921
    public function setDestroyable(?UTCDateTime $ts): bool
922
    {
923
        $this->destroy = $ts;
924
925
        if (null === $ts) {
926
            return $this->save([], 'destroy');
927
        }
928
929
        return $this->save('destroy');
930
    }
931
932
    /**
933
     * Get original raw attributes before any processing.
934
     */
935
    public function getRawAttributes(): array
936
    {
937
        return $this->raw_attributes;
938
    }
939
940
    /**
941
     * Check if node is in root.
942
     */
943
    public function isInRoot(): bool
944
    {
945
        return null === $this->parent;
946
    }
947
948
    /**
949
     * Check if node is an instance of the actual root collection.
950
     */
951
    public function isRoot(): bool
952
    {
953
        return null === $this->_id && ($this instanceof Collection);
954
    }
955
956
    /**
957
     * Resolve node path.
958
     */
959
    public function getPath(): string
960
    {
961
        $path = '';
962
        foreach (array_reverse($this->getParents()) as $parent) {
963
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
964
        }
965
966
        $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...
967
968
        return $path;
969
    }
970
971
    /**
972
     * Save node attributes.
973
     *
974
     * @param array|string $attributes
975
     * @param array|string $remove
976
     * @param string       $recursion
977
     */
978
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
979
    {
980
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
981
            throw new ForbiddenException(
982
                'not allowed to modify node '.$this->name,
983
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
984
            );
985
        }
986
987
        if ($this instanceof Collection && $this->isRoot()) {
988
            return false;
989
        }
990
991
        $remove = (array) $remove;
992
        $attributes = (array) $attributes;
993
        $this->_hook->run(
994
            'preSaveNodeAttributes',
995
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
996
        );
997
998
        try {
999
            $set = [];
1000
            $values = $this->getAttributes();
1001
            foreach ($attributes as $attr) {
1002
                $set[$attr] = $this->getArrayValue($values, $attr);
1003
            }
1004
1005
            $update = [];
1006
            if (!empty($set)) {
1007
                $update['$set'] = $set;
1008
            }
1009
1010
            if (!empty($remove)) {
1011
                $remove = array_fill_keys($remove, 1);
1012
                $update['$unset'] = $remove;
1013
            }
1014
1015
            if (empty($update)) {
1016
                return false;
1017
            }
1018
            $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...
1019
                    '_id' => $this->_id,
1020
                ], $update);
1021
1022
            $this->_hook->run(
1023
                'postSaveNodeAttributes',
1024
                [$this, $attributes, $remove, $recursion, $recursion_first]
1025
            );
1026
1027
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1028
                'category' => get_class($this),
1029
                'params' => $update,
1030
            ]);
1031
1032
            return true;
1033
        } catch (\Exception $e) {
1034
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1035
                'category' => get_class($this),
1036
                'exception' => $e,
1037
            ]);
1038
1039
            throw $e;
1040
        }
1041
    }
1042
1043
    /**
1044
     * Get array value via string path.
1045
     */
1046
    protected function getArrayValue(iterable $array, string $path, string $separator = '.')
1047
    {
1048
        if (isset($array[$path])) {
1049
            return $array[$path];
1050
        }
1051
        $keys = explode($separator, $path);
1052
1053
        foreach ($keys as $key) {
1054
            if (!array_key_exists($key, $array)) {
1055
                return;
1056
            }
1057
1058
            $array = $array[$key];
1059
        }
1060
1061
        return $array;
1062
    }
1063
1064
    /**
1065
     * Validate meta attributes.
1066
     */
1067
    protected function validateMetaAttributes(array $attributes): array
1068
    {
1069
        foreach ($attributes as $attribute => $value) {
1070
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1071
            if (!defined($const)) {
1072
                throw new Exception('meta attribute '.$attribute.' is not valid');
1073
            }
1074
1075
            if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) {
1076
                throw new Exception('tags meta attribute must be an array of strings');
1077
            }
1078
1079
            if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) {
1080
                throw new Exception($attribute.' meta attribute must be a string');
1081
            }
1082
        }
1083
1084
        return $attributes;
1085
    }
1086
1087
    /**
1088
     * Duplicate name with a uniqid within name.
1089
     */
1090
    protected function getDuplicateName(?string $name = null, ?string $class = null): string
1091
    {
1092
        if (null === $name) {
1093
            $name = $this->name;
1094
        }
1095
1096
        if (null === $class) {
1097
            $class = get_class($this);
1098
        }
1099
1100
        if ($class === Collection::class) {
1101
            return $name.' ('.substr(uniqid('', true), -4).')';
1102
        }
1103
1104
        $ext = substr(strrchr($name, '.'), 1);
1105
        if (false === $ext) {
1106
            return $name.' ('.substr(uniqid('', true), -4).')';
1107
        }
1108
1109
        $name = substr($name, 0, -(strlen($ext) + 1));
1110
1111
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1112
    }
1113
1114
    /**
1115
     * Completly remove node.
1116
     */
1117
    abstract protected function _forceDelete(): bool;
1118
}
1119