Completed
Push — master ( c6728e...bf498d )
by Raffael
14:18 queued 04:37
created

AbstractNode::setAcl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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