Completed
Push — master ( afaf42...8ac772 )
by Raffael
20:10 queued 16:21
created

AbstractNode   F

Complexity

Total Complexity 167

Size/Duplication

Total Lines 1073
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 1.06%

Importance

Changes 0
Metric Value
wmc 167
lcom 2
cbo 12
dl 0
loc 1073
ccs 4
cts 377
cp 0.0106
rs 1.308
c 0
b 0
f 0

50 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Hook;
19
use Balloon\Server;
20
use Balloon\Server\User;
21
use MimeType\MimeType;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use MongoDB\Database;
25
use Normalizer;
26
use Psr\Log\LoggerInterface;
27
use ZipStream\ZipStream;
28
29
abstract class AbstractNode implements NodeInterface
30
{
31
    /**
32
     * name max lenght.
33
     */
34
    const MAX_NAME_LENGTH = 255;
35
36
    /**
37
     * Unique id.
38
     *
39
     * @var ObjectId
40
     */
41
    protected $_id;
42
43
    /**
44
     * Node name.
45
     *
46
     * @var string
47
     */
48
    protected $name = '';
49
50
    /**
51
     * Owner.
52
     *
53
     * @var ObjectId
54
     */
55
    protected $owner;
56
57
    /**
58
     * Mime.
59
     *
60
     * @var string
61
     */
62
    protected $mime;
63
64
    /**
65
     * Meta attributes.
66
     *
67
     * @var array
68
     */
69
    protected $meta = [];
70
71
    /**
72
     * Parent collection.
73
     *
74
     * @var ObjectId
75
     */
76
    protected $parent;
77
78
    /**
79
     * Is file deleted.
80
     *
81
     * @var bool|UTCDateTime
82
     */
83
    protected $deleted = false;
84
85
    /**
86
     * Is shared?
87
     *
88
     * @var bool
89
     */
90
    protected $shared = false;
91
92
    /**
93
     * Destory at a certain time.
94
     *
95
     * @var UTCDateTime
96
     */
97
    protected $destroy;
98
99
    /**
100
     * Changed timestamp.
101
     *
102
     * @var UTCDateTime
103
     */
104
    protected $changed;
105
106
    /**
107
     * Created timestamp.
108
     *
109
     * @var UTCDateTime
110
     */
111
    protected $created;
112
113
    /**
114
     * Point to antother node (Means this node is reference to $reference).
115
     *
116
     * @var ObjectId
117
     */
118
    protected $reference;
119
120
    /**
121
     * Raw attributes before any processing or modifications.
122
     *
123
     * @var array
124
     */
125
    protected $raw_attributes;
126
127
    /**
128
     * Readonly flag.
129
     *
130
     * @var bool
131
     */
132
    protected $readonly = false;
133
134
    /**
135
     * App attributes.
136
     *
137
     * @var array
138
     */
139
    protected $app = [];
140
141
    /**
142
     * Filesystem.
143
     *
144
     * @var Filesystem
145
     */
146
    protected $_fs;
147
148
    /**
149
     * Database.
150
     *
151
     * @var Database
152
     */
153
    protected $_db;
154
155
    /**
156
     * User.
157
     *
158
     * @var User
159
     */
160
    protected $_user;
161
162
    /**
163
     * Logger.
164
     *
165
     * @var LoggerInterface
166
     */
167
    protected $_logger;
168
169
    /**
170
     * Server.
171
     *
172
     * @var Server
173
     */
174
    protected $_server;
175
176
    /**
177
     * Hook.
178
     *
179
     * @var Hook
180
     */
181
    protected $_hook;
182
183
    /**
184
     * Acl.
185
     *
186
     * @var Acl
187
     */
188
    protected $_acl;
189
190
    /**
191
     * Mount.
192
     *
193
     * @var ObjectId
194
     */
195
    protected $storage_reference;
196
197
    /**
198
     * Storage attributes.
199
     *
200
     * @var array
201
     */
202
    protected $storage;
203
204
    /**
205
     * Acl.
206
     *
207
     * @var array
208
     */
209
    protected $acl = [];
210
211
    /**
212
     * Mount.
213
     *
214
     * @var array
215
     */
216
    protected $mount = [];
217
218
    /**
219
     * Parent collection.
220
     *
221
     * @var Collection
222
     */
223
    protected $_parent;
224
225
    /**
226
     * Convert to filename.
227
     *
228
     * @return string
229
     */
230
    public function __toString()
231
    {
232
        return $this->name;
233
    }
234
235
    /**
236
     * Get owner.
237
     */
238
    public function getOwner(): ObjectId
239
    {
240
        return $this->owner;
241
    }
242
243
    /**
244
     * Set filesystem.
245
     */
246
    public function setFilesystem(Filesystem $fs): NodeInterface
247
    {
248
        $this->_fs = $fs;
249
        $this->_user = $fs->getUser();
250
251
        return $this;
252
    }
253
254
    /**
255
     * Get filesystem.
256
     */
257
    public function getFilesystem(): Filesystem
258
    {
259
        return $this->_fs;
260
    }
261
262
    /**
263
     * Check if $node is a sub node of any parent nodes of this node.
264
     */
265
    public function isSubNode(NodeInterface $node): bool
266
    {
267
        if ($node->getId() == $this->_id) {
268
            return true;
269
        }
270
271
        foreach ($node->getParents() as $node) {
272
            if ($node->getId() == $this->_id) {
273
                return true;
274
            }
275
        }
276
277
        if ($this->isRoot()) {
278
            return true;
279
        }
280
281
        return false;
282
    }
283
284
    /**
285
     * Move node.
286
     */
287
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
288
    {
289
        if ($this->parent === $parent->getId()) {
290
            throw new Exception\Conflict(
291
                'source node '.$this->name.' is already in the requested parent folder',
292
                Exception\Conflict::ALREADY_THERE
293
            );
294
        }
295
        if ($this->isSubNode($parent)) {
296
            throw new Exception\Conflict(
297
                'node called '.$this->name.' can not be moved into itself',
298
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
299
            );
300
        }
301
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
302
            throw new ForbiddenException(
303
                'not allowed to move node '.$this->name,
304
                ForbiddenException::NOT_ALLOWED_TO_MOVE
305
            );
306
        }
307
308
        $exists = $parent->childExists($this->name);
309
        if (true === $exists && NodeInterface::CONFLICT_NOACTION === $conflict) {
310
            throw new Exception\Conflict(
311
                'a node called '.$this->name.' does already exists in this collection',
312
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
313
            );
314
        }
315
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
316
            throw new Exception\Conflict(
317
                'a shared folder can not be a child of a shared folder too',
318
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
319
            );
320
        }
321
        if ($parent->isDeleted()) {
322
            throw new Exception\Conflict(
323
                'cannot move node into a deleted collction',
324
                Exception\Conflict::DELETED_PARENT
325
            );
326
        }
327
328
        if (true === $exists && NodeInterface::CONFLICT_RENAME === $conflict) {
329
            $this->setName($this->getDuplicateName());
330
            $this->raw_attributes['name'] = $this->name;
331
        }
332
333
        if ($this instanceof Collection) {
334
            $this->getChildrenRecursive($this->getRealId(), $shares);
0 ignored issues
show
Bug introduced by
The variable $shares does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
335
336
            if (!empty($shares) && $parent->isShared()) {
337
                throw new Exception\Conflict(
338
                    'folder contains a shared folder',
339
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
340
                );
341
            }
342
        }
343
344
        if (($parent->isSpecial() && $this->shared != $parent->getShareId())
345
          || (!$parent->isSpecial() && $this->isShareMember())
346
          || ($parent->getMount() != $this->getParent()->getMount())) {
347
            $new = $this->copyTo($parent, $conflict);
348
            $this->delete();
349
350
            return $new;
351
        }
352
353
        if (true === $exists && NodeInterface::CONFLICT_MERGE === $conflict) {
354
            $new = $this->copyTo($parent, $conflict);
355
            $this->delete(true);
356
357
            return $new;
358
        }
359
360
        $this->storage = $this->_parent->getStorage()->move($this, $parent);
361
        $this->parent = $parent->getRealId();
362
        $this->owner = $this->_user->getId();
363
364
        $this->save(['parent', 'shared', 'owner', 'storage']);
365
366
        return $this;
367
    }
368
369
    /**
370
     * Set node acl.
371
     */
372
    public function setAcl(array $acl): NodeInterface
373
    {
374
        if (!$this->_acl->isAllowed($this, 'm')) {
375
            throw new ForbiddenException(
376
                'not allowed to update acl',
377
                 ForbiddenException::NOT_ALLOWED_TO_MANAGE
378
            );
379
        }
380
381
        if (!$this->isShareMember()) {
382
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
383
        }
384
385
        $this->_acl->validateAcl($this->_server, $acl);
386
        $this->acl = $acl;
387
        $this->save(['acl']);
388
389
        return $this;
390
    }
391
392
    /**
393
     * Get ACL.
394
     */
395
    public function getAcl(): array
396
    {
397
        if ($this->isReference()) {
398
            $acl = $this->_fs->findRawNode($this->getShareId())['acl'];
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId() can be null; however, findRawNode() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
399
        } else {
400
            $acl = $this->acl;
401
        }
402
403
        return $this->_acl->resolveAclTable($this->_server, $acl);
404
    }
405
406
    /**
407
     * Get share id.
408
     */
409
    public function getShareId(bool $reference = false): ?ObjectId
410
    {
411
        if ($this->isReference() && true === $reference) {
412
            return $this->_id;
413
        }
414
        if ($this->isShareMember() && true === $reference) {
415
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type declared by the interface Balloon\Filesystem\Node\NodeInterface::getShareId of type MongoDB\BSON\ObjectId.

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...
416
        }
417
        if ($this->isShared() && $this->isReference()) {
418
            return $this->reference;
419
        }
420
        if ($this->isShared()) {
421
            return $this->_id;
422
        }
423
        if ($this->isShareMember()) {
424
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type declared by the interface Balloon\Filesystem\Node\NodeInterface::getShareId of type MongoDB\BSON\ObjectId.

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...
425
        }
426
427
        return null;
428
    }
429
430
    /**
431
     * Get share node.
432
     *
433
     * @return Collection
434
     */
435
    public function getShareNode(): ?Collection
436
    {
437
        if ($this->isShare()) {
438
            return $this;
439
        }
440
441
        if ($this->isSpecial()) {
442
            return $this->_fs->findNodeById($this->getShareId(true));
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId(true) can be null; however, findNodeById() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

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