Completed
Pull Request — master (#202)
by Raffael
04:41
created

Collection::childExists()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 0
cts 16
cp 0
rs 9.0648
c 0
b 0
f 0
cc 5
nc 12
nop 3
crap 30
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\Adapter\AdapterInterface as StorageAdapterInterface;
19
use Balloon\Hook;
20
use Balloon\Server\User;
21
use Generator;
22
use MimeType\MimeType;
23
use MongoDB\BSON\ObjectId;
24
use MongoDB\BSON\Regex;
25
use MongoDB\BSON\UTCDateTime;
26
use Psr\Log\LoggerInterface;
27
use Sabre\DAV\IQuota;
28
29
class Collection extends AbstractNode implements IQuota
30
{
31
    /**
32
     * Root folder.
33
     */
34
    const ROOT_FOLDER = '/';
35
36
    /**
37
     * Share acl.
38
     *
39
     * @var array
40
     */
41
    protected $acl = [];
42
43
    /**
44
     * Share name.
45
     *
46
     * @var string
47
     */
48
    protected $share_name;
49
50
    /**
51
     * filter.
52
     *
53
     * @var string
54
     */
55
    protected $filter;
56
57
    /**
58
     * Storage.
59
     *
60
     * @var StorageAdapterInterface
61
     */
62
    protected $_storage;
63
64
    /**
65
     * Initialize.
66
     */
67
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, ?Collection $parent, StorageAdapterInterface $storage)
68
    {
69
        $this->_fs = $fs;
70
        $this->_server = $fs->getServer();
71
        $this->_db = $fs->getDatabase();
72
        $this->_user = $fs->getUser();
73
        $this->_logger = $logger;
74
        $this->_hook = $hook;
75
        $this->_acl = $acl;
76
        $this->_storage = $storage;
77
        $this->_parent = $parent;
78
79
        foreach ($attributes as $attr => $value) {
80
            $this->{$attr} = $value;
81
        }
82
83
        $this->mime = 'inode/directory';
84
        $this->raw_attributes = $attributes;
85
    }
86
87
    /**
88
     * Set storage adapter.
89
     */
90
    public function setStorage(StorageAdapterInterface $adapter): self
91
    {
92
        $this->_storage = $adapter;
93
94
        return $this;
95
    }
96
97
    /**
98
     * Get storage adapter.
99
     */
100
    public function getStorage(): StorageAdapterInterface
101
    {
102
        return $this->_storage;
103
    }
104
105
    /**
106
     * Copy node with children.
107
     *
108
     * @param Collection $parent
109
     * @param string     $recursion
110
     */
111
    public function copyTo(self $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
112
    {
113
        if (null === $recursion) {
114
            $recursion_first = true;
115
            $recursion = uniqid();
116
        } else {
117
            $recursion_first = false;
118
        }
119
120
        $this->_hook->run(
121
            'preCopyCollection',
122
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
123
        );
124
125
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
126
            $name = $this->getDuplicateName();
127
        } else {
128
            $name = $this->name;
129
        }
130
131
        if ($this->_id === $parent->getId()) {
132
            throw new Exception\Conflict(
133
                'can not copy node into itself',
134
                Exception\Conflict::CANT_COPY_INTO_ITSELF
135
            );
136
        }
137
138
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
139
            $new_parent = $parent->getChild($this->name);
140
        } else {
141
            $new_parent = $parent->addDirectory($name, [
142
                'created' => $this->created,
143
                'changed' => $this->changed,
144
                'deleted' => $this->deleted,
145
                'filter' => $this->filter,
146
                'meta' => $this->meta,
147
            ], NodeInterface::CONFLICT_NOACTION, true);
148
        }
149
150
        foreach ($this->getChildNodes(NodeInterface::DELETED_INCLUDE) as $child) {
151
            $child->copyTo($new_parent, $conflict, $recursion, false);
152
        }
153
154
        $this->_hook->run(
155
            'postCopyCollection',
156
            [$this, $parent, $new_parent, $conflict, $recursion, $recursion_first]
157
        );
158
159
        return $new_parent;
160
    }
161
162
    /**
163
     * Is mount.
164
     */
165
    public function isMounted(): bool
166
    {
167
        return count($this->mount) > 0;
168
    }
169
170
    /**
171
     * Get Share name.
172
     */
173
    public function getShareName(): string
174
    {
175
        if ($this->isShare()) {
176
            return $this->share_name;
177
        }
178
179
        return $this->_fs->findRawNode($this->getShareId())['share_name'];
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...
180
    }
181
182
    /**
183
     * Get Attributes.
184
     */
185
    public function getAttributes(): array
186
    {
187
        return [
188
            '_id' => $this->_id,
189
            'name' => $this->name,
190
            'shared' => $this->shared,
191
            'share_name' => $this->share_name,
192
            'acl' => $this->acl,
193
            'directory' => true,
194
            'reference' => $this->reference,
195
            'parent' => $this->parent,
196
            'app' => $this->app,
197
            'owner' => $this->owner,
198
            'meta' => $this->meta,
199
            'mime' => $this->mime,
200
            'filter' => $this->filter,
201
            'deleted' => $this->deleted,
202
            'changed' => $this->changed,
203
            'created' => $this->created,
204
            'destroy' => $this->destroy,
205
            'readonly' => $this->readonly,
206
            'mount' => $this->mount,
207
            'storage_reference' => $this->storage_reference,
208
            'storage' => $this->storage,
209
        ];
210
    }
211
212
    /**
213
     * Set collection filter.
214
     *
215
     * @param string $filter
216
     */
217
    public function setFilter(?array $filter = null): bool
218
    {
219
        $this->filter = json_encode($filter);
220
221
        return $this->save('filter');
222
    }
223
224
    /**
225
     * Get collection.
226
     */
227
    public function get(): void
228
    {
229
        $this->getZip();
230
    }
231
232
    /**
233
     * Fetch children items of this collection.
234
     *
235
     * Deleted:
236
     *  0 - Exclude deleted
237
     *  1 - Only deleted
238
     *  2 - Include deleted
239
     *
240
     * @param int $offset
241
     * @param int $limit
242
     */
243
    public function getChildNodes(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = [], ?int $offset = null, ?int $limit = null): Generator
244
    {
245
        $filter = $this->getChildrenFilter($deleted, $filter);
246
247
        return $this->_fs->findNodesByFilter($filter, $offset, $limit);
248
    }
249
250
    /**
251
     * Fetch children items of this collection (as array).
252
     *
253
     * Deleted:
254
     *  0 - Exclude deleted
255
     *  1 - Only deleted
256
     *  2 - Include deleted
257
     */
258
    public function getChildren(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
259
    {
260
        return iterator_to_array($this->getChildNodes($deleted, $filter));
261
    }
262
263
    /**
264
     * Is custom filter node.
265
     */
266
    public function isFiltered(): bool
267
    {
268
        return !empty($this->filter);
269
    }
270
271
    /**
272
     * Get number of children.
273
     */
274
    public function getSize(): int
275
    {
276
        return $this->_db->storage->count($this->getChildrenFilter());
277
    }
278
279
    /**
280
     * Get real id (reference).
281
     *
282
     * @return ObjectId
283
     */
284
    public function getRealId(): ?ObjectId
285
    {
286
        if (true === $this->shared && $this->isReference()) {
287
            return $this->reference;
288
        }
289
290
        return $this->_id;
291
    }
292
293
    /**
294
     * Get user quota information.
295
     */
296
    public function getQuotaInfo(): array
297
    {
298
        $quota = $this->_user->getQuotaUsage();
299
300
        return [
301
            $quota['used'],
302
            $quota['available'],
303
        ];
304
    }
305
306
    /**
307
     * Fetch children items of this collection.
308
     */
309
    public function getChild($name, int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): NodeInterface
310
    {
311
        $name = $this->checkName($name);
312
        $filter = $this->getChildrenFilter($deleted, $filter);
313
        $filter['name'] = new Regex('^'.preg_quote($name).'$', 'i');
314
        $node = $this->_db->storage->findOne($filter);
315
316
        if (null === $node) {
317
            throw new Exception\NotFound(
318
                'node called '.$name.' does not exists here',
319
                Exception\NotFound::NODE_NOT_FOUND
320
            );
321
        }
322
323
        $this->_logger->debug('loaded node ['.$node['_id'].' from parent node ['.$this->getRealId().']', [
324
            'category' => get_class($this),
325
        ]);
326
327
        return $this->_fs->initNode($node);
328
    }
329
330
    /**
331
     * Delete node.
332
     *
333
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
334
     * delete finally
335
     */
336
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
337
    {
338
        if (!$this->isReference() && !$this->_acl->isAllowed($this, 'w')) {
339
            throw new ForbiddenException(
340
                'not allowed to delete node '.$this->name,
341
                ForbiddenException::NOT_ALLOWED_TO_DELETE
342
            );
343
        }
344
345
        if (null === $recursion) {
346
            $recursion_first = true;
347
            $recursion = uniqid();
348
        } else {
349
            $recursion_first = false;
350
        }
351
352
        $this->_hook->run(
353
            'preDeleteCollection',
354
            [$this, &$force, &$recursion, &$recursion_first]
355
        );
356
357
        if (true === $force) {
358
            return $this->_forceDelete($recursion, $recursion_first);
359
        }
360
361
        $this->deleted = new UTCDateTime();
362
        $this->storage = $this->_parent->getStorage()->deleteCollection($this);
363
364
        if (!$this->isReference() && !$this->isMounted() && !$this->isFiltered()) {
365
            $this->doRecursiveAction(function ($node) use ($recursion) {
366
                $node->delete(false, $recursion, false);
367
            }, NodeInterface::DELETED_EXCLUDE);
368
        }
369
370
        if (null !== $this->_id) {
371
            $result = $this->save([
372
                'deleted', 'storage',
373
            ], [], $recursion, false);
374
        } else {
375
            $result = true;
376
        }
377
378
        $this->_hook->run(
379
            'postDeleteCollection',
380
            [$this, $force, $recursion, $recursion_first]
381
        );
382
383
        return $result;
384
    }
385
386
    /**
387
     * Check if this collection has child named $name.
388
     *
389
     * deleted:
390
     *
391
     *  0 - Exclude deleted
392
     *  1 - Only deleted
393
     *  2 - Include deleted
394
     *
395
     * @param string $name
396
     * @param int    $deleted
397
     */
398
    public function childExists($name, $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): bool
399
    {
400
        $name = $this->checkName($name);
401
402
        $find = [
403
            'parent' => $this->getRealId(),
404
            'name' => new Regex('^'.preg_quote($name).'$', 'i'),
405
        ];
406
407
        if (null !== $this->_user) {
408
            $find['owner'] = $this->_user->getId();
409
        }
410
411
        switch ($deleted) {
412
            case NodeInterface::DELETED_EXCLUDE:
413
                $find['deleted'] = false;
414
415
                break;
416
            case NodeInterface::DELETED_ONLY:
417
                $find['deleted'] = ['$type' => 9];
418
419
                break;
420
        }
421
422
        $find = array_merge($filter, $find);
423
424
        if ($this->isSpecial()) {
425
            unset($find['owner']);
426
        }
427
428
        $node = $this->_db->storage->findOne($find);
429
430
        return (bool) $node;
431
    }
432
433
    /**
434
     * Share collection.
435
     */
436
    public function share(array $acl, string $name): bool
437
    {
438
        if ($this->isShareMember()) {
439
            throw new Exception('a sub node of a share can not be shared');
440
        }
441
442
        $this->_acl->validateAcl($this->_server, $acl);
443
444
        $action = [
445
            '$set' => [
446
                'shared' => $this->getRealId(),
447
            ],
448
        ];
449
450
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares);
451
452
        if (!empty($shares)) {
453
            throw new Exception('child folder contains a shared folder');
454
        }
455
456
        $this->_db->storage->updateMany([
457
            '_id' => [
458
                '$in' => $toset,
459
            ],
460
        ], $action);
461
462
        $this->_db->delta->updateMany([
463
            '_id' => [
464
                '$in' => $toset,
465
            ],
466
        ], $action);
467
468
        if ($this->getRealId() === $this->_id) {
469
            $this->acl = $acl;
470
            $this->shared = true;
471
            $this->share_name = $name;
472
            $this->save(['acl', 'shared', 'share_name']);
473
        } else {
474
            $this->_db->storage->updateOne([
475
                '_id' => $this->getRealId(),
476
            ], [
477
                '$set' => [
478
                    'share_name' => $name,
479
                    'acl' => $acl,
480
                ],
481
            ]);
482
        }
483
484
        return true;
485
    }
486
487
    /**
488
     * Unshare collection.
489
     */
490
    public function unshare(): bool
491
    {
492
        if (!$this->_acl->isAllowed($this, 'm')) {
493
            throw new ForbiddenException(
494
                'not allowed to share node',
495
                ForbiddenException::NOT_ALLOWED_TO_MANAGE
496
            );
497
        }
498
499
        if (true !== $this->shared) {
500
            throw new Exception\Conflict(
501
                'Can not unshare a none shared collection',
502
                Exception\Conflict::NOT_SHARED
503
            );
504
        }
505
506
        $this->shared = false;
507
        $this->share_name = null;
508
        $this->acl = [];
509
        $action = [
510
            '$unset' => [
511
                'shared' => $this->_id,
512
            ],
513
            '$set' => [
514
                'owner' => $this->_user->getId(),
515
            ],
516
        ];
517
518
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares);
519
520
        $this->_db->storage->updateMany([
521
            '_id' => [
522
                '$in' => $toset,
523
            ],
524
        ], $action);
525
526
        $result = $this->save(['shared'], ['acl', 'share_name']);
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...
527
528
        return true;
529
    }
530
531
    /**
532
     * Get children.
533
     */
534
    public function getChildrenRecursive(?ObjectId $id = null, ?array &$shares = []): array
535
    {
536
        $list = [];
537
        $result = $this->_db->storage->find([
538
            'parent' => $id,
539
        ], [
540
            '_id' => 1,
541
            'directory' => 1,
542
            'reference' => 1,
543
            'shared' => 1,
544
        ]);
545
546
        foreach ($result as $node) {
547
            $list[] = $node['_id'];
548
549
            if ($node['directory'] === true) {
550
                if (isset($node['reference']) || isset($node['shared']) && true === $node['shared']) {
551
                    $shares[] = $node['_id'];
552
                }
553
554
                if (true === $node['directory'] && !isset($node['reference'])) {
555
                    $list = array_merge($list, $this->getChildrenRecursive($node['_id'], $shares));
556
                }
557
            }
558
        }
559
560
        return $list;
561
    }
562
563
    /**
564
     * Create new directory.
565
     *
566
     * @param string $name
567
     * @param arracy $attributes
568
     *
569
     * @return Collection
570
     */
571
    public function addDirectory($name, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): self
572
    {
573
        if (!$this->_acl->isAllowed($this, 'w')) {
574
            throw new ForbiddenException(
575
                'not allowed to create new node here',
576
                ForbiddenException::NOT_ALLOWED_TO_CREATE
577
            );
578
        }
579
580
        $this->_hook->run('preCreateCollection', [$this, &$name, &$attributes, &$clone]);
581
582
        if ($this->readonly) {
583
            throw new Exception\Conflict(
584
                'node is set as readonly, it is not possible to add new sub nodes',
585
                Exception\Conflict::READONLY
586
            );
587
        }
588
589
        $name = $this->checkName($name);
590
591
        if ($this->childExists($name)) {
592
            if (NodeInterface::CONFLICT_NOACTION === $conflict) {
593
                throw new Exception\Conflict(
594
                    'a node called '.$name.' does already exists in this collection',
595
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
596
                );
597
            }
598
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
599
                $name = $this->getDuplicateName($name);
600
            }
601
        }
602
603
        if ($this->isDeleted()) {
604
            throw new Exception\Conflict(
605
                'could not add node '.$name.' into a deleted parent collection',
606
                Exception\Conflict::DELETED_PARENT
607
            );
608
        }
609
610
        try {
611
            $meta = [
612
                'name' => $name,
613
                'deleted' => false,
614
                'parent' => $this->getRealId(),
615
                'directory' => true,
616
                'created' => new UTCDateTime(),
617
                'changed' => new UTCDateTime(),
618
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
619
                'storage' => $this->_storage->createCollection($this, $name),
620
                'storage_reference' => $this->getMount(),
621
            ];
622
623
            if (null !== $this->_user) {
624
                $meta['owner'] = $this->_user->getId();
625
            }
626
627
            $save = array_merge($meta, $attributes);
628
629
            if (isset($save['acl'])) {
630
                $this->validateAcl($save['acl']);
631
            }
632
633
            $result = $this->_db->storage->insertOne($save, [
634
                '$isolated' => true,
635
            ]);
636
637
            $save['_id'] = $result->getInsertedId();
638
639
            $this->_logger->info('added new collection ['.$save['_id'].'] under parent ['.$this->_id.']', [
640
                'category' => get_class($this),
641
            ]);
642
643
            $this->changed = $save['changed'];
644
            $this->save('changed');
645
646
            $new = $this->_fs->initNode($save);
647
            $this->_hook->run('postCreateCollection', [$this, $new, $clone]);
648
649
            return $new;
650
        } catch (\Exception $e) {
651
            $this->_logger->error('failed create new collection under parent ['.$this->_id.']', [
652
                'category' => get_class($this),
653
                'exception' => $e,
654
            ]);
655
656
            throw $e;
657
        }
658
    }
659
660
    /**
661
     * Create new file as a child from this collection.
662
     */
663
    public function addFile($name, ?ObjectId $session = null, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): File
664
    {
665
        if (!$this->_acl->isAllowed($this, 'w')) {
666
            throw new ForbiddenException(
667
                'not allowed to create new node here',
668
                ForbiddenException::NOT_ALLOWED_TO_CREATE
669
            );
670
        }
671
672
        $this->_hook->run('preCreateFile', [$this, &$name, &$attributes, &$clone]);
673
674
        if ($this->readonly) {
675
            throw new Exception\Conflict(
676
                'node is set as readonly, it is not possible to add new sub nodes',
677
                Exception\Conflict::READONLY
678
            );
679
        }
680
681
        $name = $this->checkName($name);
682
683
        if ($this->childExists($name)) {
684
            if (NodeInterface::CONFLICT_NOACTION === $conflict) {
685
                throw new Exception\Conflict(
686
                    'a node called '.$name.' does already exists in this collection',
687
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
688
                );
689
            }
690
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
691
                $name = $this->getDuplicateName($name, File::class);
692
            }
693
        }
694
695
        if ($this->isDeleted()) {
696
            throw new Exception\Conflict(
697
                'could not add node '.$name.' into a deleted parent collection',
698
                Exception\Conflict::DELETED_PARENT
699
            );
700
        }
701
702
        try {
703
            $meta = [
704
                'name' => $name,
705
                'deleted' => false,
706
                'parent' => $this->getRealId(),
707
                'directory' => false,
708
                'hash' => null,
709
                'mime' => MimeType::getType($name),
710
                'created' => new UTCDateTime(),
711
                'changed' => new UTCDateTime(),
712
                'version' => 0,
713
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
714
                'storage_reference' => $this->getMount(),
715
            ];
716
717
            if (null !== $this->_user) {
718
                $meta['owner'] = $this->_user->getId();
719
            }
720
721
            $save = array_merge($meta, $attributes);
722
723
            if (isset($save['acl'])) {
724
                $this->validateAcl($save['acl']);
725
            }
726
727
            $result = $this->_db->storage->insertOne($save, [
728
                '$isolated' => true,
729
            ]);
730
731
            $save['_id'] = $result->getInsertedId();
732
733
            $this->_logger->info('added new file ['.$save['_id'].'] under parent ['.$this->_id.']', [
734
                'category' => get_class($this),
735
            ]);
736
737
            $this->changed = $save['changed'];
738
            $this->save('changed');
739
740
            $file = $this->_fs->initNode($save);
741
742
            if ($session !== null) {
743
                $file->setContent($session, $attributes);
744
            }
745
746
            $this->_hook->run('postCreateFile', [$this, $file, $clone]);
747
748
            return $file;
749
        } catch (\Exception $e) {
750
            $this->_logger->error('failed add new file under parent ['.$this->_id.']', [
751
                'category' => get_class($this),
752
                'exception' => $e,
753
            ]);
754
755
            throw $e;
756
        }
757
    }
758
759
    /**
760
     * Create new file wrapper
761
     * (Sabe\DAV compatible method, elsewhere use addFile().
762
     *
763
     * Sabre\DAV requires that createFile() returns the ETag instead the newly created file instance
764
     *
765
     * @param string $name
766
     * @param string $data
767
     */
768
    public function createFile($name, $data = null): string
769
    {
770
        $session = $this->_storage->storeTemporaryFile($data, $this->_user);
771
772
        if ($this->childExists($name)) {
773
            $file = $this->getChild($name);
774
            $file->setContent($session);
775
        } else {
776
            $file = $this->addFile($name, $session);
777
        }
778
779
        return $file->getETag();
0 ignored issues
show
Bug introduced by
The method getETag does only exist in Balloon\Filesystem\Node\File, but not in Balloon\Filesystem\Node\NodeInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
780
    }
781
782
    /**
783
     * Create new directory wrapper
784
     * (Sabe\DAV compatible method, elsewhere use addDirectory().
785
     *
786
     * Sabre\DAV requires that createDirectory() returns void
787
     *
788
     * @param string $name
789
     */
790
    public function createDirectory($name): void
791
    {
792
        $this->addDirectory($name);
793
    }
794
795
    /**
796
     * Do recursive Action.
797
     */
798
    public function doRecursiveAction(callable $callable, int $deleted = NodeInterface::DELETED_EXCLUDE): bool
799
    {
800
        $children = $this->getChildNodes($deleted, []);
801
802
        foreach ($children as $child) {
803
            $callable($child);
804
        }
805
806
        return true;
807
    }
808
809
    /**
810
     * Validate acl.
811
     */
812
    protected function validateAcl(array $acl): bool
813
    {
814
        if (!$this->_acl->isAllowed($this, 'm')) {
815
            throw new ForbiddenException(
816
                 'not allowed to set acl',
817
                  ForbiddenException::NOT_ALLOWED_TO_MANAGE
818
            );
819
        }
820
821
        if (!$this->isSpecial()) {
822
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
823
        }
824
825
        $this->_acl->validateAcl($this->_server, $acl);
826
827
        return true;
828
    }
829
830
    /**
831
     * Get children query filter.
832
     *
833
     * Deleted:
834
     *  0 - Exclude deleted
835
     *  1 - Only deleted
836
     *  2 - Include deleted
837
     */
838
    protected function getChildrenFilter(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
839
    {
840
        $search = [
841
            'parent' => $this->getRealId(),
842
        ];
843
844
        if (NodeInterface::DELETED_EXCLUDE === $deleted) {
845
            $search['deleted'] = false;
846
        } elseif (NodeInterface::DELETED_ONLY === $deleted) {
847
            $search['deleted'] = ['$type' => 9];
848
        }
849
850
        $search = array_merge($filter, $search);
851
852
        if ($this->shared) {
853
            $search = [
854
                '$and' => [
855
                    $search,
856
                    [
857
                        '$or' => [
858
                            ['shared' => $this->reference],
859
                            ['shared' => $this->shared],
860
                            ['shared' => $this->_id],
861
                        ],
862
                    ],
863
                ],
864
            ];
865
        } elseif (null !== $this->_user) {
866
            $search['owner'] = $this->_user->getId();
867
        }
868
869
        if ($this->filter !== null && $this->_user !== null) {
870
            $include = isset($search['deleted']) ? ['deleted' => $search['deleted']] : [];
871
            $stored_filter = ['$and' => [
872
                array_merge(
873
                    $include,
874
                    json_decode($this->filter, true),
875
                    $filter
876
                ),
877
                ['$or' => [
878
                    ['owner' => $this->_user->getId()],
879
                    ['shared' => ['$in' => $this->_user->getShares()]],
880
                ]],
881
            ]];
882
883
            $search = ['$or' => [
884
                $search,
885
                $stored_filter,
886
            ]];
887
        }
888
889
        return $search;
890
    }
891
892
    /**
893
     * Completely remove node.
894
     */
895
    protected function _forceDelete(?string $recursion = null, bool $recursion_first = true): bool
896
    {
897
        if (!$this->isReference() && !$this->isMounted()) {
898
            $this->doRecursiveAction(function ($node) use ($recursion) {
899
                $node->delete(true, $recursion, false);
900
            }, NodeInterface::DELETED_INCLUDE);
901
        }
902
903
        try {
904
            $this->_parent->getStorage()->forceDeleteCollection($this);
905
            $result = $this->_db->storage->deleteOne(['_id' => $this->_id]);
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...
906
907
            if ($this->isShared()) {
908
                $result = $this->_db->storage->deleteMany(['reference' => $this->_id]);
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...
909
            }
910
911
            $this->_logger->info('force removed collection ['.$this->_id.']', [
912
                'category' => get_class($this),
913
            ]);
914
915
            $this->_hook->run(
916
                'postDeleteCollection',
917
                [$this, true, $recursion, $recursion_first]
918
            );
919
        } catch (\Exception $e) {
920
            $this->_logger->error('failed force remove collection ['.$this->_id.']', [
921
                'category' => get_class($this),
922
                'exception' => $e,
923
            ]);
924
925
            throw $e;
926
        }
927
928
        return true;
929
    }
930
}
931