Completed
Push — master ( 2b3f4c...5ada1f )
by Raffael
77:32 queued 61:21
created

AbstractNode   F

Complexity

Total Complexity 183

Size/Duplication

Total Lines 1139
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 18

Test Coverage

Coverage 1.04%

Importance

Changes 0
Metric Value
wmc 183
lcom 2
cbo 18
dl 0
loc 1139
ccs 4
cts 383
cp 0.0104
rs 1.044
c 0
b 0
f 0

57 Methods

Rating   Name   Duplication   Size   Complexity  
A getDuplicateName() 0 23 5
A __toString() 0 4 1
A getOwner() 0 4 1
A setFilesystem() 0 7 1
A getFilesystem() 0 4 1
A isSubNode() 0 18 5
D setParent() 0 64 22
A lock() 0 13 3
A getLock() 0 8 2
A isLocked() 0 11 3
A unlock() 0 19 5
A setAcl() 0 16 3
A getAcl() 0 10 2
B getShareId() 0 20 9
A getReference() 0 4 1
A getShareNode() 0 12 3
A isReadonly() 0 4 1
A mayWrite() 0 4 1
A isOwnerRequest() 0 4 2
A isSpecial() 0 14 4
A isShareMember() 0 4 2
A isMountMember() 0 4 1
A isShare() 0 4 2
A isShared() 0 8 2
A setName() 0 22 4
A checkName() 0 15 4
A getName() 0 4 1
A getMount() 0 4 2
C undelete() 0 50 13
A isDeleted() 0 4 1
A getLastModified() 0 8 2
A getId() 0 4 1
A getParent() 0 4 1
A getParents() 0 14 3
A getZip() 0 7 1
C zip() 0 45 14
A getContentType() 0 4 1
A isReference() 0 4 1
A setAppAttributes() 0 7 1
A setAppAttribute() 0 11 2
A unsetAppAttributes() 0 9 2
A unsetAppAttribute() 0 9 2
A getAppAttribute() 0 8 2
A getAppAttributes() 0 8 2
A setMetaAttributes() 0 15 5
A getMetaAttributes() 0 9 3
A setReadonly() 0 7 1
A setDestroyable() 0 10 2
A getRawAttributes() 0 4 1
A isInRoot() 0 4 1
A isRoot() 0 4 2
A getPath() 0 11 2
C save() 0 61 10
A prepareLock() 0 9 1
A getArrayValue() 0 17 4
B validateMetaAttributes() 0 19 9
_forceDelete() 0 1 ?

How to fix   Complexity   

Complex Class

Complex classes like AbstractNode often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractNode, and based on these observations, apply Extract Interface, too.

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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Balloon\Filesystem\Node\AbstractNode) is incompatible with the return type declared by the interface Balloon\Filesystem\Node\...nterface::setFilesystem of type self.

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...
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('source node '.$this->name.' is already in the requested parent folder', Exception\Conflict::ALREADY_THERE);
305
        }
306
        if ($this->isSubNode($parent)) {
307
            throw new Exception\Conflict('node called '.$this->name.' can not be moved into itself', Exception\Conflict::CANT_BE_CHILD_OF_ITSELF);
308
        }
309
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
310
            throw new ForbiddenException('not allowed to move node '.$this->name, ForbiddenException::NOT_ALLOWED_TO_MOVE);
311
        }
312
313
        $new_name = $parent->validateInsert($this->name, $conflict, get_class($this));
314
315
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
316
            throw new Exception\Conflict('a shared folder can not be a child of a shared folder', Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE);
317
        }
318
319
        if (NodeInterface::CONFLICT_RENAME === $conflict && $new_name !== $this->name) {
320
            $this->setName($new_name);
321
            $this->raw_attributes['name'] = $this->name;
322
        }
323
324
        if ($this instanceof Collection) {
325
            $query = [
326
                '$or' => [
327
                    ['reference' => ['exists' => true]],
328
                    ['shared' => true],
329
                ],
330
            ];
331
332
            if ($parent->isShared() && iterator_count($this->_fs->findNodesByFilterRecursive($this, $query, 0, 1)) !== 0) {
0 ignored issues
show
Compatibility introduced by
$this of type object<Balloon\Filesystem\Node\AbstractNode> is not a sub-type of object<Balloon\Filesystem\Node\Collection>. It seems like you assume a child class of the class Balloon\Filesystem\Node\AbstractNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
333
                throw new Exception\Conflict('folder contains a shared folder', Exception\Conflict::NODE_CONTAINS_SHARED_NODE);
334
            }
335
        }
336
337
        if ($this->isShared() && $parent->isSpecial()) {
338
            throw new Exception\Conflict('a shared folder can not be an indirect child of a shared folder', Exception\Conflict::SHARED_NODE_CANT_BE_INDIRECT_CHILD_OF_SHARE);
339
        }
340
341
        if (($parent->isSpecial() && $this->shared != $parent->getShareId())
342
          || (!$parent->isSpecial() && $this->isShareMember())
343
          || ($parent->getMount() != $this->getParent()->getMount())) {
344
            $new = $this->copyTo($parent, $conflict);
345
            $this->delete();
346
347
            return $new;
348
        }
349
350
        if ($parent->childExists($this->name) && NodeInterface::CONFLICT_MERGE === $conflict) {
351
            $new = $this->copyTo($parent, $conflict);
352
            $this->delete(true);
353
354
            return $new;
355
        }
356
357
        $this->storage = $this->_parent->getStorage()->move($this, $parent);
358
        $this->parent = $parent->getRealId();
359
        $this->owner = $this->_user->getId();
360
361
        $this->save(['parent', 'shared', 'owner', 'storage']);
362
363
        return $this;
364
    }
365
366
    /**
367
     * Lock file.
368
     */
369
    public function lock(string $identifier, ?int $ttl = 1800): NodeInterface
370
    {
371
        if ($this->isLocked()) {
372
            if ($identifier !== $this->lock['id']) {
373
                throw new Exception\LockIdMissmatch('the unlock id must match the current lock id');
374
            }
375
        }
376
377
        $this->lock = $this->prepareLock($identifier, $ttl ?? 1800);
378
        $this->save(['lock']);
379
380
        return $this;
381
    }
382
383
    /**
384
     * Get lock.
385
     */
386
    public function getLock(): array
387
    {
388
        if (!$this->isLocked()) {
389
            throw new Exception\NotLocked('node is not locked');
390
        }
391
392
        return $this->lock;
393
    }
394
395
    /**
396
     * Is locked?
397
     */
398
    public function isLocked(): bool
399
    {
400
        if ($this->lock === null) {
401
            return false;
402
        }
403
        if ($this->lock['expire'] <= new UTCDateTime()) {
404
            return false;
405
        }
406
407
        return true;
408
    }
409
410
    /**
411
     * Unlock.
412
     */
413
    public function unlock(?string $identifier = null): NodeInterface
414
    {
415
        if (!$this->isLocked()) {
416
            throw new Exception\NotLocked('node is not locked');
417
        }
418
419
        if ($this->lock['owner'] != $this->_user->getId()) {
420
            throw new Exception\Forbidden('node is locked by another user');
421
        }
422
423
        if ($identifier !== null && $this->lock['id'] !== $identifier) {
424
            throw new Exception\LockIdMissmatch('the unlock id must match the current lock id');
425
        }
426
427
        $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...
428
        $this->save(['lock']);
429
430
        return $this;
431
    }
432
433
    /**
434
     * Set node acl.
435
     */
436
    public function setAcl(array $acl): NodeInterface
437
    {
438
        if (!$this->_acl->isAllowed($this, 'm')) {
439
            throw new ForbiddenException('not allowed to update acl', ForbiddenException::NOT_ALLOWED_TO_MANAGE);
440
        }
441
442
        if (!$this->isShareMember()) {
443
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
444
        }
445
446
        $this->_acl->validateAcl($this->_server, $acl);
447
        $this->acl = $acl;
448
        $this->save(['acl']);
449
450
        return $this;
451
    }
452
453
    /**
454
     * Get ACL.
455
     */
456
    public function getAcl(): array
457
    {
458
        if ($this->isReference()) {
459
            $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...
460
        } else {
461
            $acl = $this->acl;
462
        }
463
464
        return $this->_acl->resolveAclTable($this->_server, $acl);
465
    }
466
467
    /**
468
     * Get share id.
469
     */
470
    public function getShareId(bool $reference = false): ?ObjectId
471
    {
472
        if ($this->isReference() && true === $reference) {
473
            return $this->_id;
474
        }
475
        if ($this->isShareMember() && true === $reference) {
476
            return $this->shared;
477
        }
478
        if ($this->isShared() && $this->isReference()) {
479
            return $this->reference;
480
        }
481
        if ($this->isShared()) {
482
            return $this->_id;
483
        }
484
        if ($this->isShareMember()) {
485
            return $this->shared;
486
        }
487
488
        return null;
489
    }
490
491
    /**
492
     * Get reference.
493
     */
494
    public function getReference(): ?ObjectId
495
    {
496
        return $this->reference;
497
    }
498
499
    /**
500
     * Get share node.
501
     */
502
    public function getShareNode(): ?Collection
503
    {
504
        if ($this->isShare()) {
505
            return $this;
506
        }
507
508
        if ($this->isSpecial()) {
509
            return $this->_fs->findNodeById($this->getShareId(true));
510
        }
511
512
        return null;
513
    }
514
515
    /**
516
     * Is node marked as readonly?
517
     */
518
    public function isReadonly(): bool
519
    {
520
        return $this->readonly;
521
    }
522
523
    /**
524
     * May write.
525
     */
526
    public function mayWrite(): bool
527
    {
528
        return Acl::PRIVILEGES_WEIGHT[$this->_acl->getAclPrivilege($this)] > Acl::PRIVILEGE_READ;
529
    }
530
531
    /**
532
     * Request is from node owner?
533
     */
534
    public function isOwnerRequest(): bool
535
    {
536
        return null !== $this->_user && $this->owner == $this->_user->getId();
537
    }
538
539
    /**
540
     * Check if node is kind of special.
541
     */
542
    public function isSpecial(): bool
543
    {
544
        if ($this->isShared()) {
545
            return true;
546
        }
547
        if ($this->isReference()) {
548
            return true;
549
        }
550
        if ($this->isShareMember()) {
551
            return true;
552
        }
553
554
        return false;
555
    }
556
557
    /**
558
     * Check if node is a sub node of a share.
559
     */
560
    public function isShareMember(): bool
561
    {
562
        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...
563
    }
564
565
    /**
566
     * Check if node is a sub node of an external storage mount.
567
     */
568
    public function isMountMember(): bool
569
    {
570
        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...
571
    }
572
573
    /**
574
     * Is share.
575
     */
576
    public function isShare(): bool
577
    {
578
        return true === $this->shared && !$this->isReference();
579
    }
580
581
    /**
582
     * Is share (Reference or master share).
583
     */
584
    public function isShared(): bool
585
    {
586
        if (true === $this->shared) {
587
            return true;
588
        }
589
590
        return false;
591
    }
592
593
    /**
594
     * Set the name.
595
     */
596
    public function setName($name): bool
597
    {
598
        $name = $this->checkName($name);
599
600
        try {
601
            $child = $this->getParent()->getChild($name);
602
            if ($child->getId() != $this->_id) {
603
                throw new Exception\Conflict('a node called '.$name.' does already exists in this collection', Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS);
604
            }
605
        } catch (Exception\NotFound $e) {
606
            //child does not exists, we can safely rename
607
        }
608
609
        $this->storage = $this->_parent->getStorage()->rename($this, $name);
610
        $this->name = $name;
611
612
        if ($this instanceof File) {
613
            $this->mime = MimeType::getType($this->name);
614
        }
615
616
        return $this->save(['name', 'storage', 'mime']);
617
    }
618
619
    /**
620
     * Check name.
621
     */
622
    public function checkName(string $name): string
623
    {
624
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
625
            throw new Exception\InvalidArgument('name contains invalid characters');
626
        }
627
        if (strlen($name) > self::MAX_NAME_LENGTH) {
628
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
629
        }
630
631
        if (!Normalizer::isNormalized($name)) {
632
            $name = Normalizer::normalize($name);
633
        }
634
635
        return $name;
636
    }
637
638
    /**
639
     * Get the name.
640
     */
641
    public function getName(): string
642
    {
643
        return $this->name;
644
    }
645
646
    /**
647
     * Get mount node.
648
     */
649
    public function getMount(): ?ObjectId
650
    {
651
        return count($this->mount) > 0 ? $this->_id : $this->storage_reference;
652
    }
653
654
    /**
655
     * Undelete.
656
     */
657
    public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool
658
    {
659
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
660
            throw new ForbiddenException('not allowed to restore node '.$this->name, ForbiddenException::NOT_ALLOWED_TO_UNDELETE);
661
        }
662
663
        $parent = $this->getParent();
664
        if ($parent->isDeleted()) {
665
            throw new Exception\Conflict('could not restore node '.$this->name.' into a deleted parent', Exception\Conflict::DELETED_PARENT);
666
        }
667
668
        if ($parent->childExists($this->name)) {
669
            if (NodeInterface::CONFLICT_MERGE === $conflict) {
670
                $new = $this->copyTo($parent, $conflict, null, true, NodeInterface::DELETED_INCLUDE);
671
672
                if ($new->getId() != $this->getId()) {
673
                    $this->delete(true);
674
                }
675
            } elseif (NodeInterface::CONFLICT_RENAME === $conflict) {
676
                $this->setName($this->getDuplicateName());
677
                $this->raw_attributes['name'] = $this->name;
678
            } else {
679
                throw new Exception\Conflict('a node called '.$this->name.' does already exists in this collection', Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS);
680
            }
681
        }
682
683
        if (null === $recursion) {
684
            $recursion_first = true;
685
            $recursion = uniqid();
686
        } else {
687
            $recursion_first = false;
688
        }
689
690
        $this->storage = $this->_parent->getStorage()->undelete($this);
691
        $this->deleted = false;
692
693
        $this->save([
694
                'storage',
695
                'name',
696
                'deleted',
697
            ], [], $recursion, $recursion_first);
698
699
        if ($this instanceof File || $this->isReference() || $this->isMounted() || $this->isFiltered()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Balloon\Filesystem\Node\AbstractNode as the method isMounted() does only exist in the following sub-classes of Balloon\Filesystem\Node\AbstractNode: Balloon\Filesystem\Node\Collection. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Balloon\Filesystem\Node\AbstractNode as the method isFiltered() does only exist in the following sub-classes of Balloon\Filesystem\Node\AbstractNode: Balloon\Filesystem\Node\Collection. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
700
            return true;
701
        }
702
703
        return $this->doRecursiveAction(function ($node) use ($conflict, $recursion) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Balloon\Filesystem\Node\AbstractNode as the method doRecursiveAction() does only exist in the following sub-classes of Balloon\Filesystem\Node\AbstractNode: Balloon\Filesystem\Node\Collection. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
704
            $node->undelete($conflict, $recursion, false);
705
        }, NodeInterface::DELETED_ONLY);
706
    }
707
708
    /**
709
     * Is node deleted?
710
     */
711
    public function isDeleted(): bool
712
    {
713
        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...
714
    }
715
716
    /**
717
     * Get last modified timestamp.
718
     */
719
    public function getLastModified(): int
720
    {
721
        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...
722
            return (int) $this->changed->toDateTime()->format('U');
723
        }
724
725
        return 0;
726
    }
727
728
    /**
729
     * Get unique id.
730
     */
731 1
    public function getId(): ?ObjectId
732
    {
733 1
        return $this->_id;
734
    }
735
736
    /**
737
     * Get parent.
738
     */
739
    public function getParent(): ?Collection
740
    {
741
        return $this->_parent;
742
    }
743
744
    /**
745
     * Get parents.
746
     */
747
    public function getParents(?NodeInterface $node = null, array $parents = []): array
748
    {
749
        if (null === $node) {
750
            $node = $this;
751
        }
752
753
        if ($node->isInRoot()) {
754
            return $parents;
755
        }
756
        $parent = $node->getParent();
757
        $parents[] = $parent;
758
759
        return $node->getParents($parent, $parents);
760
    }
761
762
    /**
763
     * Get as zip.
764
     */
765
    public function getZip(): void
766
    {
767
        set_time_limit(0);
768
        $archive = new ZipStream($this->name.'.zip');
769
        $this->zip($archive, false);
770
        $archive->finish();
771
    }
772
773
    /**
774
     * Create zip.
775
     */
776
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
777
    {
778
        if (null === $parent) {
779
            $parent = $this;
780
        }
781
782
        if ($parent instanceof Collection) {
783
            $children = $parent->getChildNodes();
784
785
            if (true === $self && 0 === $depth) {
786
                $path = $parent->getName().DIRECTORY_SEPARATOR;
787
            } elseif (0 === $depth) {
788
                $path = '';
789
            } elseif (0 !== $depth) {
790
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
791
            }
792
793
            foreach ($children as $child) {
794
                $name = $path.$child->getName();
795
796
                if ($child instanceof Collection) {
797
                    $this->zip($archive, $self, $child, $name, ++$depth);
798
                } elseif ($child instanceof File) {
799
                    try {
800
                        $resource = $child->get();
801
                        if ($resource !== null) {
802
                            $archive->addFileFromStream($name, $resource);
803
                        }
804
                    } catch (\Exception $e) {
805
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
806
                            'category' => get_class($this),
807
                            'exception' => $e,
808
                        ]);
809
                    }
810
                }
811
            }
812
        } elseif ($parent instanceof File) {
813
            $resource = $parent->get();
814
            if ($resource !== null) {
815
                $archive->addFileFromStream($parent->getName(), $resource);
816
            }
817
        }
818
819
        return true;
820
    }
821
822
    /**
823
     * Get mime type.
824
     */
825 1
    public function getContentType(): string
826
    {
827 1
        return $this->mime;
828
    }
829
830
    /**
831
     * Is reference.
832
     */
833
    public function isReference(): bool
834
    {
835
        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...
836
    }
837
838
    /**
839
     * Set app attributes.
840
     */
841
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
842
    {
843
        $this->app[$namespace] = $attributes;
844
        $this->save('app.'.$namespace);
845
846
        return $this;
847
    }
848
849
    /**
850
     * Set app attribute.
851
     */
852
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
853
    {
854
        if (!isset($this->app[$namespace])) {
855
            $this->app[$namespace] = [];
856
        }
857
858
        $this->app[$namespace][$attribute] = $value;
859
        $this->save('app.'.$namespace);
860
861
        return $this;
862
    }
863
864
    /**
865
     * Remove app attribute.
866
     */
867
    public function unsetAppAttributes(string $namespace): NodeInterface
868
    {
869
        if (isset($this->app[$namespace])) {
870
            unset($this->app[$namespace]);
871
            $this->save('app.'.$namespace);
872
        }
873
874
        return $this;
875
    }
876
877
    /**
878
     * Remove app attribute.
879
     */
880
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
881
    {
882
        if (isset($this->app[$namespace][$attribute])) {
883
            unset($this->app[$namespace][$attribute]);
884
            $this->save('app'.$namespace);
885
        }
886
887
        return $this;
888
    }
889
890
    /**
891
     * Get app attribute.
892
     */
893
    public function getAppAttribute(string $namespace, string $attribute)
894
    {
895
        if (isset($this->app[$namespace][$attribute])) {
896
            return $this->app[$namespace][$attribute];
897
        }
898
899
        return null;
900
    }
901
902
    /**
903
     * Get app attributes.
904
     */
905
    public function getAppAttributes(string $namespace): array
906
    {
907
        if (isset($this->app[$namespace])) {
908
            return $this->app[$namespace];
909
        }
910
911
        return [];
912
    }
913
914
    /**
915
     * Set meta attributes.
916
     */
917
    public function setMetaAttributes(array $attributes): NodeInterface
918
    {
919
        $attributes = $this->validateMetaAttributes($attributes);
920
        foreach ($attributes as $attribute => $value) {
921
            if (empty($value) && isset($this->meta[$attribute])) {
922
                unset($this->meta[$attribute]);
923
            } elseif (!empty($value)) {
924
                $this->meta[$attribute] = $value;
925
            }
926
        }
927
928
        $this->save('meta');
929
930
        return $this;
931
    }
932
933
    /**
934
     * Get meta attributes as array.
935
     */
936
    public function getMetaAttributes(array $attributes = []): array
937
    {
938
        if (empty($attributes)) {
939
            return $this->meta;
940
        }
941
        if (is_array($attributes)) {
942
            return array_intersect_key($this->meta, array_flip($attributes));
943
        }
944
    }
945
946
    /**
947
     * Mark node as readonly.
948
     */
949
    public function setReadonly(bool $readonly = true): bool
950
    {
951
        $this->readonly = $readonly;
952
        $this->storage = $this->_parent->getStorage()->readonly($this, $readonly);
953
954
        return $this->save(['readonly', 'storage']);
955
    }
956
957
    /**
958
     * Mark node as self-destroyable.
959
     */
960
    public function setDestroyable(?UTCDateTime $ts): bool
961
    {
962
        $this->destroy = $ts;
963
964
        if (null === $ts) {
965
            return $this->save([], 'destroy');
966
        }
967
968
        return $this->save('destroy');
969
    }
970
971
    /**
972
     * Get original raw attributes before any processing.
973
     */
974
    public function getRawAttributes(): array
975
    {
976
        return $this->raw_attributes;
977
    }
978
979
    /**
980
     * Check if node is in root.
981
     */
982
    public function isInRoot(): bool
983
    {
984
        return null === $this->parent;
985
    }
986
987
    /**
988
     * Check if node is an instance of the actual root collection.
989
     */
990
    public function isRoot(): bool
991
    {
992
        return null === $this->_id && ($this instanceof Collection);
993
    }
994
995
    /**
996
     * Resolve node path.
997
     */
998
    public function getPath(): string
999
    {
1000
        $path = '';
1001
        foreach (array_reverse($this->getParents()) as $parent) {
1002
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1003
        }
1004
1005
        $path .= DIRECTORY_SEPARATOR.$this->getName();
1006
1007
        return $path;
1008
    }
1009
1010
    /**
1011
     * Save node attributes.
1012
     *
1013
     * @param array|string $attributes
1014
     * @param array|string $remove
1015
     * @param string       $recursion
1016
     */
1017
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
1018
    {
1019
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
1020
            throw new ForbiddenException('not allowed to modify node '.$this->name, ForbiddenException::NOT_ALLOWED_TO_MODIFY);
1021
        }
1022
1023
        if ($this instanceof Collection && $this->isRoot()) {
1024
            return false;
1025
        }
1026
1027
        $remove = (array) $remove;
1028
        $attributes = (array) $attributes;
1029
        $this->_hook->run(
1030
            'preSaveNodeAttributes',
1031
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
1032
        );
1033
1034
        try {
1035
            $set = [];
1036
            $values = $this->getAttributes();
1037
            foreach ($attributes as $attr) {
1038
                $set[$attr] = $this->getArrayValue($values, $attr);
1039
            }
1040
1041
            $update = [];
1042
            if (!empty($set)) {
1043
                $update['$set'] = $set;
1044
            }
1045
1046
            if (!empty($remove)) {
1047
                $remove = array_fill_keys($remove, 1);
1048
                $update['$unset'] = $remove;
1049
            }
1050
1051
            if (empty($update)) {
1052
                return false;
1053
            }
1054
            $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...
1055
                    '_id' => $this->_id,
1056
                ], $update);
1057
1058
            $this->_hook->run(
1059
                'postSaveNodeAttributes',
1060
                [$this, $attributes, $remove, $recursion, $recursion_first]
1061
            );
1062
1063
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1064
                'category' => get_class($this),
1065
                'params' => $update,
1066
            ]);
1067
1068
            return true;
1069
        } catch (\Exception $e) {
1070
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1071
                'category' => get_class($this),
1072
                'exception' => $e,
1073
            ]);
1074
1075
            throw $e;
1076
        }
1077
    }
1078
1079
    /**
1080
     * Duplicate name with a uniqid within name.
1081
     */
1082
    public function getDuplicateName(?string $name = null, ?string $class = null): string
1083
    {
1084
        if (null === $name) {
1085
            $name = $this->name;
1086
        }
1087
1088
        if (null === $class) {
1089
            $class = get_class($this);
1090
        }
1091
1092
        if ($class === Collection::class) {
1093
            return $name.' ('.substr(uniqid('', true), -4).')';
1094
        }
1095
1096
        $ext = substr(strrchr($name, '.'), 1);
1097
        if (false === $ext) {
1098
            return $name.' ('.substr(uniqid('', true), -4).')';
1099
        }
1100
1101
        $name = substr($name, 0, -(strlen($ext) + 1));
1102
1103
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1104
    }
1105
1106
    /**
1107
     * Prepare lock.
1108
     */
1109
    protected function prepareLock(string $identifier, int $ttl = 1800): array
1110
    {
1111
        return [
1112
             'owner' => $this->_user->getId(),
1113
            'created' => new UTCDateTime(),
1114
            'id' => $identifier,
1115
            'expire' => new UTCDateTime((time() + $ttl) * 1000),
1116
        ];
1117
    }
1118
1119
    /**
1120
     * Get array value via string path.
1121
     */
1122
    protected function getArrayValue(iterable $array, string $path, string $separator = '.')
1123
    {
1124
        if (isset($array[$path])) {
1125
            return $array[$path];
1126
        }
1127
        $keys = explode($separator, $path);
1128
1129
        foreach ($keys as $key) {
1130
            if (!array_key_exists($key, $array)) {
1131
                return;
1132
            }
1133
1134
            $array = $array[$key];
1135
        }
1136
1137
        return $array;
1138
    }
1139
1140
    /**
1141
     * Validate meta attributes.
1142
     */
1143
    protected function validateMetaAttributes(array $attributes): array
1144
    {
1145
        foreach ($attributes as $attribute => $value) {
1146
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1147
            if (!defined($const)) {
1148
                throw new Exception('meta attribute '.$attribute.' is not valid');
1149
            }
1150
1151
            if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) {
1152
                throw new Exception('tags meta attribute must be an array of strings');
1153
            }
1154
1155
            if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) {
1156
                throw new Exception($attribute.' meta attribute must be a string');
1157
            }
1158
        }
1159
1160
        return $attributes;
1161
    }
1162
1163
    /**
1164
     * Completly remove node.
1165
     */
1166
    abstract protected function _forceDelete(): bool;
1167
}
1168