Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

AbstractNode::unlock()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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