Test Failed
Push — master ( 87f942...2b3f4c )
by Raffael
53:31 queued 51:46
created

AbstractNode::getLock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
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(
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) {
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...
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()) {
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...
733
            return true;
734
        }
735
736
        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...
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
        set_time_limit(0);
801
        $archive = new ZipStream($this->name.'.zip');
802
        $this->zip($archive, false);
803
        $archive->finish();
804
    }
805
806
    /**
807
     * Create zip.
808
     */
809
    public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool
810
    {
811
        if (null === $parent) {
812
            $parent = $this;
813
        }
814
815
        if ($parent instanceof Collection) {
816
            $children = $parent->getChildNodes();
817
818
            if (true === $self && 0 === $depth) {
819
                $path = $parent->getName().DIRECTORY_SEPARATOR;
820
            } elseif (0 === $depth) {
821
                $path = '';
822
            } elseif (0 !== $depth) {
823
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
824
            }
825
826
            foreach ($children as $child) {
827
                $name = $path.$child->getName();
828
829
                if ($child instanceof Collection) {
830
                    $this->zip($archive, $self, $child, $name, ++$depth);
831
                } elseif ($child instanceof File) {
832
                    try {
833
                        $resource = $child->get();
834
                        if ($resource !== null) {
835
                            $archive->addFileFromStream($name, $resource);
836
                        }
837
                    } catch (\Exception $e) {
838
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [
839
                            'category' => get_class($this),
840
                            'exception' => $e,
841
                        ]);
842
                    }
843
                }
844
            }
845
        } elseif ($parent instanceof File) {
846
            $resource = $parent->get();
847
            if ($resource !== null) {
848
                $archive->addFileFromStream($parent->getName(), $resource);
849
            }
850
        }
851
852
        return true;
853
    }
854
855
    /**
856
     * Get mime type.
857
     */
858 1
    public function getContentType(): string
859
    {
860 1
        return $this->mime;
861
    }
862
863
    /**
864
     * Is reference.
865
     */
866
    public function isReference(): bool
867
    {
868
        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...
869
    }
870
871
    /**
872
     * Set app attributes.
873
     */
874
    public function setAppAttributes(string $namespace, array $attributes): NodeInterface
875
    {
876
        $this->app[$namespace] = $attributes;
877
        $this->save('app.'.$namespace);
878
879
        return $this;
880
    }
881
882
    /**
883
     * Set app attribute.
884
     */
885
    public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface
886
    {
887
        if (!isset($this->app[$namespace])) {
888
            $this->app[$namespace] = [];
889
        }
890
891
        $this->app[$namespace][$attribute] = $value;
892
        $this->save('app.'.$namespace);
893
894
        return $this;
895
    }
896
897
    /**
898
     * Remove app attribute.
899
     */
900
    public function unsetAppAttributes(string $namespace): NodeInterface
901
    {
902
        if (isset($this->app[$namespace])) {
903
            unset($this->app[$namespace]);
904
            $this->save('app.'.$namespace);
905
        }
906
907
        return $this;
908
    }
909
910
    /**
911
     * Remove app attribute.
912
     */
913
    public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface
914
    {
915
        if (isset($this->app[$namespace][$attribute])) {
916
            unset($this->app[$namespace][$attribute]);
917
            $this->save('app'.$namespace);
918
        }
919
920
        return $this;
921
    }
922
923
    /**
924
     * Get app attribute.
925
     */
926
    public function getAppAttribute(string $namespace, string $attribute)
927
    {
928
        if (isset($this->app[$namespace][$attribute])) {
929
            return $this->app[$namespace][$attribute];
930
        }
931
932
        return null;
933
    }
934
935
    /**
936
     * Get app attributes.
937
     */
938
    public function getAppAttributes(string $namespace): array
939
    {
940
        if (isset($this->app[$namespace])) {
941
            return $this->app[$namespace];
942
        }
943
944
        return [];
945
    }
946
947
    /**
948
     * Set meta attributes.
949
     */
950
    public function setMetaAttributes(array $attributes): NodeInterface
951
    {
952
        $attributes = $this->validateMetaAttributes($attributes);
953
        foreach ($attributes as $attribute => $value) {
954
            if (empty($value) && isset($this->meta[$attribute])) {
955
                unset($this->meta[$attribute]);
956
            } elseif (!empty($value)) {
957
                $this->meta[$attribute] = $value;
958
            }
959
        }
960
961
        $this->save('meta');
962
963
        return $this;
964
    }
965
966
    /**
967
     * Get meta attributes as array.
968
     */
969
    public function getMetaAttributes(array $attributes = []): array
970
    {
971
        if (empty($attributes)) {
972
            return $this->meta;
973
        }
974
        if (is_array($attributes)) {
975
            return array_intersect_key($this->meta, array_flip($attributes));
976
        }
977
    }
978
979
    /**
980
     * Mark node as readonly.
981
     */
982
    public function setReadonly(bool $readonly = true): bool
983
    {
984
        $this->readonly = $readonly;
985
        $this->storage = $this->_parent->getStorage()->readonly($this, $readonly);
986
987
        return $this->save(['readonly', 'storage']);
988
    }
989
990
    /**
991
     * Mark node as self-destroyable.
992
     */
993
    public function setDestroyable(?UTCDateTime $ts): bool
994
    {
995
        $this->destroy = $ts;
996
997
        if (null === $ts) {
998
            return $this->save([], 'destroy');
999
        }
1000
1001
        return $this->save('destroy');
1002
    }
1003
1004
    /**
1005
     * Get original raw attributes before any processing.
1006
     */
1007
    public function getRawAttributes(): array
1008
    {
1009
        return $this->raw_attributes;
1010
    }
1011
1012
    /**
1013
     * Check if node is in root.
1014
     */
1015
    public function isInRoot(): bool
1016
    {
1017
        return null === $this->parent;
1018
    }
1019
1020
    /**
1021
     * Check if node is an instance of the actual root collection.
1022
     */
1023
    public function isRoot(): bool
1024
    {
1025
        return null === $this->_id && ($this instanceof Collection);
1026
    }
1027
1028
    /**
1029
     * Resolve node path.
1030
     */
1031
    public function getPath(): string
1032
    {
1033
        $path = '';
1034
        foreach (array_reverse($this->getParents()) as $parent) {
1035
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1036
        }
1037
1038
        $path .= DIRECTORY_SEPARATOR.$this->getName();
1039
1040
        return $path;
1041
    }
1042
1043
    /**
1044
     * Save node attributes.
1045
     *
1046
     * @param array|string $attributes
1047
     * @param array|string $remove
1048
     * @param string       $recursion
1049
     */
1050
    public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool
1051
    {
1052
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
1053
            throw new ForbiddenException(
1054
                'not allowed to modify node '.$this->name,
1055
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
1056
            );
1057
        }
1058
1059
        if ($this instanceof Collection && $this->isRoot()) {
1060
            return false;
1061
        }
1062
1063
        $remove = (array) $remove;
1064
        $attributes = (array) $attributes;
1065
        $this->_hook->run(
1066
            'preSaveNodeAttributes',
1067
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]
1068
        );
1069
1070
        try {
1071
            $set = [];
1072
            $values = $this->getAttributes();
1073
            foreach ($attributes as $attr) {
1074
                $set[$attr] = $this->getArrayValue($values, $attr);
1075
            }
1076
1077
            $update = [];
1078
            if (!empty($set)) {
1079
                $update['$set'] = $set;
1080
            }
1081
1082
            if (!empty($remove)) {
1083
                $remove = array_fill_keys($remove, 1);
1084
                $update['$unset'] = $remove;
1085
            }
1086
1087
            if (empty($update)) {
1088
                return false;
1089
            }
1090
            $result = $this->_db->storage->updateOne([
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1091
                    '_id' => $this->_id,
1092
                ], $update);
1093
1094
            $this->_hook->run(
1095
                'postSaveNodeAttributes',
1096
                [$this, $attributes, $remove, $recursion, $recursion_first]
1097
            );
1098
1099
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1100
                'category' => get_class($this),
1101
                'params' => $update,
1102
            ]);
1103
1104
            return true;
1105
        } catch (\Exception $e) {
1106
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1107
                'category' => get_class($this),
1108
                'exception' => $e,
1109
            ]);
1110
1111
            throw $e;
1112
        }
1113
    }
1114
1115
    /**
1116
     * Duplicate name with a uniqid within name.
1117
     */
1118
    public function getDuplicateName(?string $name = null, ?string $class = null): string
1119
    {
1120
        if (null === $name) {
1121
            $name = $this->name;
1122
        }
1123
1124
        if (null === $class) {
1125
            $class = get_class($this);
1126
        }
1127
1128
        if ($class === Collection::class) {
1129
            return $name.' ('.substr(uniqid('', true), -4).')';
1130
        }
1131
1132
        $ext = substr(strrchr($name, '.'), 1);
1133
        if (false === $ext) {
1134
            return $name.' ('.substr(uniqid('', true), -4).')';
1135
        }
1136
1137
        $name = substr($name, 0, -(strlen($ext) + 1));
1138
1139
        return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
1140
    }
1141
1142
    /**
1143
     * Prepare lock.
1144
     */
1145
    protected function prepareLock(string $identifier, int $ttl = 1800): array
1146
    {
1147
        return [
1148
             'owner' => $this->_user->getId(),
1149
            'created' => new UTCDateTime(),
1150
            'id' => $identifier,
1151
            'expire' => new UTCDateTime((time() + $ttl) * 1000),
1152
        ];
1153
    }
1154
1155
    /**
1156
     * Get array value via string path.
1157
     */
1158
    protected function getArrayValue(iterable $array, string $path, string $separator = '.')
1159
    {
1160
        if (isset($array[$path])) {
1161
            return $array[$path];
1162
        }
1163
        $keys = explode($separator, $path);
1164
1165
        foreach ($keys as $key) {
1166
            if (!array_key_exists($key, $array)) {
1167
                return;
1168
            }
1169
1170
            $array = $array[$key];
1171
        }
1172
1173
        return $array;
1174
    }
1175
1176
    /**
1177
     * Validate meta attributes.
1178
     */
1179
    protected function validateMetaAttributes(array $attributes): array
1180
    {
1181
        foreach ($attributes as $attribute => $value) {
1182
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1183
            if (!defined($const)) {
1184
                throw new Exception('meta attribute '.$attribute.' is not valid');
1185
            }
1186
1187
            if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) {
1188
                throw new Exception('tags meta attribute must be an array of strings');
1189
            }
1190
1191
            if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) {
1192
                throw new Exception($attribute.' meta attribute must be a string');
1193
            }
1194
        }
1195
1196
        return $attributes;
1197
    }
1198
1199
    /**
1200
     * Completly remove node.
1201
     */
1202
    abstract protected function _forceDelete(): bool;
1203
}
1204