Completed
Pull Request — master (#141)
by Raffael
15:37 queued 10:36
created

AbstractNode::temporarySession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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\Filesystem\Storage;
19
use Balloon\Hook;
20
use Balloon\Server;
21
use Balloon\Server\User;
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
     * Storage adapter.
192
     *
193
     * @var string
194
     */
195
    protected $storage_adapter;
196
197
    /**
198
     * Acl.
199
     *
200
     * @var Acl
201
     */
202
    protected $acl;
203
204
    /**
205
     * Storage.
206
     *
207
     * @var Storage
208
     */
209
    protected $_storage;
210
211
    /**
212
     * Convert to filename.
213
     *
214
     * @return string
215
     */
216
    public function __toString()
217
    {
218
        return $this->name;
219
    }
220
221
    /**
222
     * Get owner.
223
     */
224
    public function getOwner(): ObjectId
225
    {
226
        return $this->owner;
227
    }
228
229
    /**
230
     * Set filesystem.
231
     */
232
    public function setFilesystem(Filesystem $fs): NodeInterface
233
    {
234
        $this->_fs = $fs;
235
        $this->_user = $fs->getUser();
236
237
        return $this;
238
    }
239
240
    /**
241
     * Get filesystem.
242
     */
243
    public function getFilesystem(): Filesystem
244
    {
245
        return $this->_fs;
246
    }
247
248
    /**
249
     * temporary session.
250
     */
251
    public function temporarySession($stream, ObjectId $session = null): ObjectId
252
    {
253
        return $this->_storage->storeTemporaryFile($stream, $this->_user, $session);
254
    }
255
256
    /**
257
     * Check if $node is a sub node of any parent nodes of this node.
258
     */
259
    public function isSubNode(NodeInterface $node): bool
260
    {
261
        if ($node->getId() == $this->_id) {
262
            return true;
263
        }
264
265
        foreach ($node->getParents() as $node) {
266
            if ($node->getId() == $this->_id) {
267
                return true;
268
            }
269
        }
270
271
        if ($this->isRoot()) {
272
            return true;
273
        }
274
275
        return false;
276
    }
277
278
    /**
279
     * Move node.
280
     */
281
    public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface
282
    {
283
        if ($this->parent === $parent->getId()) {
284
            throw new Exception\Conflict(
285
                'source node '.$this->name.' is already in the requested parent folder',
286
                Exception\Conflict::ALREADY_THERE
287
            );
288
        }
289
        if ($this->isSubNode($parent)) {
290
            throw new Exception\Conflict(
291
                'node called '.$this->name.' can not be moved into itself',
292
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
293
            );
294
        }
295
        if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) {
296
            throw new ForbiddenException(
297
                'not allowed to move node '.$this->name,
298
                ForbiddenException::NOT_ALLOWED_TO_MOVE
299
            );
300
        }
301
302
        $exists = $parent->childExists($this->name);
303
        if (true === $exists && NodeInterface::CONFLICT_NOACTION === $conflict) {
304
            throw new Exception\Conflict(
305
                'a node called '.$this->name.' does already exists in this collection',
306
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
307
            );
308
        }
309
        if ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
310
            throw new Exception\Conflict(
311
                'a shared folder can not be a child of a shared folder too',
312
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
313
            );
314
        }
315
        if ($parent->isDeleted()) {
316
            throw new Exception\Conflict(
317
                'cannot move node into a deleted collction',
318
                Exception\Conflict::DELETED_PARENT
319
            );
320
        }
321
322
        if (true === $exists && NodeInterface::CONFLICT_RENAME === $conflict) {
323
            $this->setName($this->getDuplicateName());
324
            $this->raw_attributes['name'] = $this->name;
325
        }
326
327
        if ($this instanceof Collection) {
328
            $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...
329
330
            if (!empty($shares) && $parent->isShared()) {
331
                throw new Exception\Conflict(
332
                    'folder contains a shared folder',
333
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
334
                );
335
            }
336
        }
337
338
        if ($parent->isSpecial() && $this->shared !== $parent->getShareId() || !$parent->isSpecial() && $this->isShareMember()) {
339
            $new = $this->copyTo($parent, $conflict);
340
            $this->delete();
341
342
            return $new;
343
        }
344
345
        if (true === $exists && NodeInterface::CONFLICT_MERGE === $conflict) {
346
            $new = $this->copyTo($parent, $conflict);
347
            $this->delete(true/*, false, false*/);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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