Completed
Push — master ( b97427...e235cc )
by Raffael
30:35 queued 26:08
created

Collection::addFile()   C

Complexity

Conditions 8
Paths 185

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
dl 0
loc 71
ccs 0
cts 40
cp 0
rs 6.8216
c 0
b 0
f 0
cc 8
nc 185
nop 5
crap 72

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Filesystem\Storage\Adapter\AdapterInterface as StorageAdapterInterface;
19
use Balloon\Hook;
20
use Balloon\Server\User;
21
use Generator;
22
use MimeType\MimeType;
23
use function MongoDB\BSON\fromJSON;
24
use MongoDB\BSON\ObjectId;
25
use MongoDB\BSON\Regex;
26
use function MongoDB\BSON\toPHP;
27
use MongoDB\BSON\UTCDateTime;
28
use Psr\Log\LoggerInterface;
29
use Sabre\DAV\IQuota;
30
31
class Collection extends AbstractNode implements IQuota
32
{
33
    /**
34
     * Root folder.
35
     */
36
    const ROOT_FOLDER = '/';
37
38
    /**
39
     * Share acl.
40
     *
41
     * @var array
42
     */
43
    protected $acl = [];
44
45
    /**
46
     * Share name.
47
     *
48
     * @var string
49
     */
50
    protected $share_name;
51
52
    /**
53
     * filter.
54
     *
55
     * @var string
56
     */
57
    protected $filter;
58
59
    /**
60
     * Storage.
61
     *
62
     * @var StorageAdapterInterface
63
     */
64
    protected $_storage;
65
66
    /**
67
     * Initialize.
68
     */
69
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, ?Collection $parent, StorageAdapterInterface $storage)
70
    {
71
        $this->_fs = $fs;
72
        $this->_server = $fs->getServer();
73
        $this->_db = $fs->getDatabase();
74
        $this->_user = $fs->getUser();
75
        $this->_logger = $logger;
76
        $this->_hook = $hook;
77
        $this->_acl = $acl;
78
        $this->_storage = $storage;
79
        $this->_parent = $parent;
80
81
        foreach ($attributes as $attr => $value) {
82
            $this->{$attr} = $value;
83
        }
84
85
        $this->mime = 'inode/directory';
86
        $this->raw_attributes = $attributes;
87
    }
88
89
    /**
90
     * Set storage adapter.
91
     */
92
    public function setStorage(StorageAdapterInterface $adapter): self
93
    {
94
        $this->_storage = $adapter;
95
96
        return $this;
97
    }
98
99
    /**
100
     * Get storage adapter.
101
     */
102
    public function getStorage(): StorageAdapterInterface
103
    {
104
        return $this->_storage;
105
    }
106
107
    /**
108
     * Copy node with children.
109
     */
110
    public function copyTo(self $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true, int $deleted = NodeInterface::DELETED_EXCLUDE): NodeInterface
111
    {
112
        if (null === $recursion) {
113
            $recursion_first = true;
114
            $recursion = uniqid();
115
        } else {
116
            $recursion_first = false;
117
        }
118
119
        $this->_hook->run(
120
            'preCopyCollection',
121
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
122
        );
123
124
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
125
            $name = $this->getDuplicateName();
126
        } else {
127
            $name = $this->name;
128
        }
129
130
        if ($this->_id === $parent->getId()) {
131
            throw new Exception\Conflict(
132
                'can not copy node into itself',
133
                Exception\Conflict::CANT_COPY_INTO_ITSELF
134
            );
135
        }
136
137
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
138
            $new_parent = $parent->getChild($this->name);
139
140
            if ($new_parent instanceof File) {
141
                $new_parent = $this;
142
            }
143
        } else {
144
            $new_parent = $parent->addDirectory($name, [
145
                'created' => $this->created,
146
                'changed' => $this->changed,
147
                'filter' => $this->filter,
148
                'meta' => $this->meta,
149
            ], NodeInterface::CONFLICT_NOACTION, true);
150
        }
151
152
        foreach ($this->getChildNodes($deleted) as $child) {
153
            $child->copyTo($new_parent, $conflict, $recursion, false, $deleted);
154
        }
155
156
        $this->_hook->run(
157
            'postCopyCollection',
158
            [$this, $parent, $new_parent, $conflict, $recursion, $recursion_first]
159
        );
160
161
        return $new_parent;
162
    }
163
164
    /**
165
     * Is mount.
166
     */
167
    public function isMounted(): bool
168
    {
169
        return count($this->mount) > 0;
170
    }
171
172
    /**
173
     * Get Share name.
174
     */
175
    public function getShareName(): string
176
    {
177
        if ($this->isShare()) {
178
            return $this->share_name;
179
        }
180
181
        return $this->_fs->findRawNode($this->getShareId())['share_name'];
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId() targeting Balloon\Filesystem\Node\AbstractNode::getShareId() can also be of type boolean or null; however, Balloon\Filesystem::findRawNode() does only seem to accept object<MongoDB\BSON\ObjectId>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
182
    }
183
184
    /**
185
     * Get Attributes.
186
     */
187
    public function getAttributes(): array
188
    {
189
        return [
190
            '_id' => $this->_id,
191
            'name' => $this->name,
192
            'shared' => $this->shared,
193
            'share_name' => $this->share_name,
194
            'acl' => $this->acl,
195
            'lock' => $this->lock,
196
            'directory' => true,
197
            'reference' => $this->reference,
198
            'parent' => $this->parent,
199
            'app' => $this->app,
200
            'owner' => $this->owner,
201
            'meta' => $this->meta,
202
            'mime' => $this->mime,
203
            'filter' => $this->filter,
204
            'deleted' => $this->deleted,
205
            'changed' => $this->changed,
206
            'created' => $this->created,
207
            'destroy' => $this->destroy,
208
            'readonly' => $this->readonly,
209
            'mount' => $this->mount,
210
            'storage_reference' => $this->storage_reference,
211
            'storage' => $this->storage,
212
        ];
213
    }
214
215
    /**
216
     * Set collection filter.
217
     */
218
    public function setFilter(?array $filter = null): bool
219
    {
220
        $this->filter = json_encode($filter, JSON_THROW_ON_ERROR);
221
222
        return $this->save('filter');
223
    }
224
225
    /**
226
     * Get collection.
227
     */
228
    public function get(): void
229
    {
230
        $this->getZip();
231
    }
232
233
    /**
234
     * Fetch children items of this collection.
235
     *
236
     * Deleted:
237
     *  0 - Exclude deleted
238
     *  1 - Only deleted
239
     *  2 - Include deleted
240
     */
241
    public function getChildNodes(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = [], ?int $offset = null, ?int $limit = null, bool $recursive = false): Generator
242
    {
243
        $filter = $this->getChildrenFilter($deleted, $filter);
244
245
        if ($recursive === false) {
246
            return $this->_fs->findNodesByFilterRecursiveChildren($filter, $deleted, $offset, $limit);
247
        }
248
249
        unset($filter['parent']);
250
251
        return $this->_fs->findNodesByFilterRecursive($this, $filter, $offset, $limit);
252
    }
253
254
    /**
255
     * Fetch children items of this collection (as array).
256
     *
257
     * Deleted:
258
     *  0 - Exclude deleted
259
     *  1 - Only deleted
260
     *  2 - Include deleted
261
     */
262
    public function getChildren(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
263
    {
264
        return iterator_to_array($this->getChildNodes($deleted, $filter));
265
    }
266
267
    /**
268
     * Is custom filter node.
269
     */
270
    public function isFiltered(): bool
271
    {
272
        return !empty($this->filter);
273
    }
274
275
    /**
276
     * Get number of children.
277
     */
278
    public function getSize(): int
279
    {
280
        if ($this->isFiltered()) {
281
            return count($this->getChildren());
282
        }
283
284
        return $this->size;
285
    }
286
287
    /**
288
     * Get real id (reference).
289
     *
290
     * @return ObjectId
291
     */
292
    public function getRealId(): ?ObjectId
293
    {
294
        if (true === $this->shared && $this->isReference()) {
295
            return $this->reference;
296
        }
297
298
        return $this->_id;
299
    }
300
301
    /**
302
     * Get user quota information.
303
     */
304
    public function getQuotaInfo(): array
305
    {
306
        $quota = $this->_user->getQuotaUsage();
307
308
        return [
309
            $quota['used'],
310
            $quota['available'],
311
        ];
312
    }
313
314
    /**
315
     * Fetch children items of this collection.
316
     */
317
    public function getChild($name, int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): NodeInterface
318
    {
319
        $name = $this->checkName($name);
320
        $filter = $this->getChildrenFilter($deleted, $filter);
321
        $filter['name'] = new Regex('^'.preg_quote($name).'$', 'i');
322
        $node = $this->_db->storage->findOne($filter);
323
324
        if (null === $node) {
325
            throw new Exception\NotFound(
326
                'node called '.$name.' does not exists here',
327
                Exception\NotFound::NODE_NOT_FOUND
328
            );
329
        }
330
331
        $this->_logger->debug('loaded node ['.$node['_id'].' from parent node ['.$this->getRealId().']', [
332
            'category' => get_class($this),
333
        ]);
334
335
        return $this->_fs->initNode($node);
336
    }
337
338
    /**
339
     * Delete node.
340
     *
341
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
342
     * delete finally
343
     */
344
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
345
    {
346
        if (!$this->isReference() && !$this->_acl->isAllowed($this, 'w')) {
347
            throw new ForbiddenException(
348
                'not allowed to delete node '.$this->name,
349
                ForbiddenException::NOT_ALLOWED_TO_DELETE
350
            );
351
        }
352
353
        if (null === $recursion) {
354
            $recursion_first = true;
355
            $recursion = uniqid();
356
        } else {
357
            $recursion_first = false;
358
        }
359
360
        $this->_hook->run(
361
            'preDeleteCollection',
362
            [$this, &$force, &$recursion, &$recursion_first]
363
        );
364
365
        if (true === $force) {
366
            return $this->_forceDelete($recursion, $recursion_first);
367
        }
368
369
        $this->deleted = new UTCDateTime();
370
        $this->storage = $this->_parent->getStorage()->deleteCollection($this);
371
372
        if (!$this->isReference() && !$this->isMounted() && !$this->isFiltered()) {
373
            $this->doRecursiveAction(function ($node) use ($recursion) {
374
                $node->delete(false, $recursion, false);
375
            }, NodeInterface::DELETED_EXCLUDE);
376
        }
377
378
        if (null !== $this->_id) {
379
            $result = $this->save([
380
                'deleted', 'storage',
381
            ], [], $recursion, false);
382
        } else {
383
            $result = true;
384
        }
385
386
        $this->_hook->run(
387
            'postDeleteCollection',
388
            [$this, $force, $recursion, $recursion_first]
389
        );
390
391
        return $result;
392
    }
393
394
    /**
395
     * Check if this collection has child named $name.
396
     *
397
     * deleted:
398
     *
399
     *  0 - Exclude deleted
400
     *  1 - Only deleted
401
     *  2 - Include deleted
402
     *
403
     * @param string $name
404
     * @param int    $deleted
405
     */
406
    public function childExists($name, $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): bool
407
    {
408
        $name = $this->checkName($name);
409
410
        $find = [
411
            'parent' => $this->getRealId(),
412
            'name' => new Regex('^'.preg_quote($name).'$', 'i'),
413
        ];
414
415
        if (null !== $this->_user) {
416
            $find['owner'] = $this->_user->getId();
417
        }
418
419
        switch ($deleted) {
420
            case NodeInterface::DELETED_EXCLUDE:
421
                $find['deleted'] = false;
422
423
                break;
424
            case NodeInterface::DELETED_ONLY:
425
                $find['deleted'] = ['$type' => 9];
426
427
                break;
428
        }
429
430
        $find = array_merge($filter, $find);
431
432
        if ($this->isSpecial()) {
433
            unset($find['owner']);
434
        }
435
436
        $node = $this->_db->storage->findOne($find);
437
438
        return (bool) $node;
439
    }
440
441
    /**
442
     * Share collection.
443
     */
444
    public function share(array $acl, string $name): bool
445
    {
446
        if ($this->isShareMember()) {
447
            throw new Exception('a sub node of a share can not be shared');
448
        }
449
450
        $this->checkName($name);
451
        $this->_acl->validateAcl($this->_server, $acl);
452
453
        $action = [
454
            '$set' => [
455
                'shared' => $this->getRealId(),
456
            ],
457
        ];
458
459
        $query = [
460
            '$or' => [
461
                ['reference' => ['exists' => true]],
462
                ['shared' => true],
463
            ],
464
        ];
465
466
        if (iterator_count($this->_fs->findNodesByFilterRecursive($this, $query, 0, 1)) !== 0) {
467
            throw new Exception\Conflict(
468
                'folder contains a shared folder',
469
                Exception\Conflict::NODE_CONTAINS_SHARED_NODE
470
            );
471
        }
472
473
        $toset = $this->_fs->findNodesByFilterRecursiveToArray($this);
474
        $this->_db->storage->updateMany([
475
            '_id' => [
476
                '$in' => $toset,
477
            ],
478
        ], $action);
479
480
        $this->_db->delta->updateMany([
481
            '_id' => [
482
                '$in' => $toset,
483
            ],
484
        ], $action);
485
486
        if ($this->getRealId() === $this->_id) {
487
            $this->acl = $acl;
488
            $this->shared = true;
489
            $this->share_name = $name;
490
            $this->save(['acl', 'shared', 'share_name']);
491
        } else {
492
            $this->_db->storage->updateOne([
493
                '_id' => $this->getRealId(),
494
            ], [
495
                '$set' => [
496
                    'share_name' => $name,
497
                    'acl' => $acl,
498
                ],
499
            ]);
500
        }
501
502
        return true;
503
    }
504
505
    /**
506
     * Unshare collection.
507
     */
508
    public function unshare(): Collection
509
    {
510
        if (!$this->_acl->isAllowed($this, 'm')) {
511
            throw new ForbiddenException(
512
                'not allowed to share node',
513
                ForbiddenException::NOT_ALLOWED_TO_MANAGE
514
            );
515
        }
516
517
        if (true !== $this->shared) {
518
            throw new Exception\Conflict(
519
                'Can not unshare a none shared collection',
520
                Exception\Conflict::NOT_SHARED
521
            );
522
        }
523
524
        $real = $this->getRealId();
525
526
        $action = [
527
            '$set' => [
528
                'owner' => $this->_fs->findRawNode($real)['owner'],
529
                'shared' => false,
530
            ],
531
        ];
532
533
        $this->shared = false;
534
        $this->share_name = null;
535
        $this->acl = [];
536
537
        $toset = $this->_fs->findNodesByFilterRecursiveToArray($this);
538
        $this->_db->storage->updateMany([
539
            '_id' => [
540
                '$in' => $toset,
541
            ],
542
        ], $action);
543
544
        if ($real === $this->_id) {
545
            $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...
546
        } else {
547
            $this->_db->storage->updateOne([
548
                '_id' => $real,
549
            ], [
550
                '$set' => [
551
                    'share_name' => null,
552
                    'acl' => [],
553
                    'shared' => false,
554
                ],
555
            ]);
556
557
            $this->delete();
558
        }
559
560
        return $this;
561
    }
562
563
    /**
564
     * Create new directory.
565
     */
566
    public function addDirectory($name, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): self
567
    {
568
        if (!$this->_acl->isAllowed($this, 'w')) {
569
            throw new ForbiddenException(
570
                'not allowed to create new node here',
571
                ForbiddenException::NOT_ALLOWED_TO_CREATE
572
            );
573
        }
574
575
        $this->_hook->run('preCreateCollection', [$this, &$name, &$attributes, &$clone]);
576
        $name = $this->validateInsert($name, $conflict, Collection::class);
577
        $id = new ObjectId();
578
579
        if (isset($attributes['lock'])) {
580
            $attributes['lock'] = $this->prepareLock($attributes['lock']);
581
        }
582
583
        try {
584
            $meta = [
585
                '_id' => $id,
586
                'pointer' => $id,
587
                'name' => $name,
588
                'deleted' => false,
589
                'parent' => $this->getRealId(),
590
                'directory' => true,
591
                'created' => new UTCDateTime(),
592
                'changed' => new UTCDateTime(),
593
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
594
                'storage' => $this->_storage->createCollection($this, $name),
595
                'storage_reference' => $this->getMount(),
596
            ];
597
598
            if (null !== $this->_user) {
599
                $meta['owner'] = $this->_user->getId();
600
            }
601
602
            $save = array_merge($meta, $attributes);
603
604
            if (isset($save['filter'])) {
605
                $this->validateFilter($save['filter']);
606
            }
607
608
            if (isset($save['acl'])) {
609
                $this->validateAcl($save['acl']);
610
            }
611
612
            $result = $this->_db->storage->insertOne($save);
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...
613
614
            $this->_logger->info('added new collection ['.$save['_id'].'] under parent ['.$this->_id.']', [
615
                'category' => get_class($this),
616
            ]);
617
618
            $this->changed = $save['changed'];
619
            $this->save('changed');
620
621
            $new = $this->_fs->initNode($save);
622
            $this->_hook->run('postCreateCollection', [$this, $new, $clone]);
623
624
            return $new;
625
        } catch (\Exception $e) {
626
            $this->_logger->error('failed create new collection under parent ['.$this->_id.']', [
627
                'category' => get_class($this),
628
                'exception' => $e,
629
            ]);
630
631
            throw $e;
632
        }
633
    }
634
635
    /**
636
     * Create new file as a child from this collection.
637
     */
638
    public function addFile($name, ?ObjectId $session = null, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): File
639
    {
640
        if (!$this->_acl->isAllowed($this, 'w')) {
641
            throw new ForbiddenException(
642
                'not allowed to create new node here',
643
                ForbiddenException::NOT_ALLOWED_TO_CREATE
644
            );
645
        }
646
647
        $this->_hook->run('preCreateFile', [$this, &$name, &$attributes, &$clone]);
648
        $name = $this->validateInsert($name, $conflict, File::class);
649
        $id = new ObjectId();
650
651
        if (isset($attributes['lock'])) {
652
            $attributes['lock'] = $this->prepareLock($attributes['lock']);
653
        }
654
655
        try {
656
            $meta = [
657
                '_id' => $id,
658
                'pointer' => $id,
659
                'name' => $name,
660
                'deleted' => false,
661
                'parent' => $this->getRealId(),
662
                'directory' => false,
663
                'hash' => null,
664
                'mime' => MimeType::getType($name),
665
                'created' => new UTCDateTime(),
666
                'changed' => new UTCDateTime(),
667
                'version' => 0,
668
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
669
                'storage_reference' => $this->getMount(),
670
            ];
671
672
            if (null !== $this->_user) {
673
                $meta['owner'] = $this->_user->getId();
674
            }
675
676
            $save = array_merge($meta, $attributes);
677
678
            if (isset($save['acl'])) {
679
                $this->validateAcl($save['acl']);
680
            }
681
682
            $result = $this->_db->storage->insertOne($save);
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...
683
684
            $this->_logger->info('added new file ['.$save['_id'].'] under parent ['.$this->_id.']', [
685
                'category' => get_class($this),
686
            ]);
687
688
            $this->changed = $save['changed'];
689
            $this->save('changed');
690
691
            $file = $this->_fs->initNode($save);
692
693
            if ($session !== null) {
694
                $file->setContent($session, $attributes);
695
            }
696
697
            $this->_hook->run('postCreateFile', [$this, $file, $clone]);
698
699
            return $file;
700
        } catch (\Exception $e) {
701
            $this->_logger->error('failed add new file under parent ['.$this->_id.']', [
702
                'category' => get_class($this),
703
                'exception' => $e,
704
            ]);
705
706
            throw $e;
707
        }
708
    }
709
710
    /**
711
     * Create new file wrapper
712
     * (Sabe\DAV compatible method, elsewhere use addFile().
713
     *
714
     * Sabre\DAV requires that createFile() returns the ETag instead the newly created file instance
715
     *
716
     * @param string $name
717
     * @param string $data
718
     */
719
    public function createFile($name, $data = null): string
720
    {
721
        $session = $this->_storage->storeTemporaryFile($data, $this->_user);
722
723
        if ($this->childExists($name, NodeInterface::DELETED_INCLUDE, ['directory' => false])) {
724
            $file = $this->getChild($name, NodeInterface::DELETED_INCLUDE, ['directory' => false]);
725
            $file->setContent($session);
726
        } else {
727
            $file = $this->addFile($name, $session);
728
        }
729
730
        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...
731
    }
732
733
    /**
734
     * Create new directory wrapper
735
     * (Sabe\DAV compatible method, elsewhere use addDirectory().
736
     *
737
     * Sabre\DAV requires that createDirectory() returns void
738
     *
739
     * @param string $name
740
     */
741
    public function createDirectory($name): void
742
    {
743
        $this->addDirectory($name);
744
    }
745
746
    /**
747
     * Do recursive Action.
748
     */
749
    public function doRecursiveAction(callable $callable, int $deleted = NodeInterface::DELETED_EXCLUDE): bool
750
    {
751
        $children = $this->getChildNodes($deleted, []);
752
753
        foreach ($children as $child) {
754
            $callable($child);
755
        }
756
757
        return true;
758
    }
759
760
    /**
761
     * Validate insert.
762
     */
763
    public function validateInsert(string $name, int $conflict = NodeInterface::CONFLICT_NOACTION, string $type = Collection::class): string
764
    {
765
        if ($this->readonly) {
766
            throw new Exception\Conflict(
767
                'node is set as readonly, it is not possible to add new sub nodes',
768
                Exception\Conflict::READONLY
769
            );
770
        }
771
772
        if ($this->isFiltered()) {
773
            throw new Exception\Conflict(
774
                'could not add node '.$name.' into a filtered parent collection',
775
                Exception\Conflict::DYNAMIC_PARENT
776
            );
777
        }
778
779
        if ($this->isDeleted()) {
780
            throw new Exception\Conflict(
781
                'could not add node '.$name.' into a deleted parent collection',
782
                Exception\Conflict::DELETED_PARENT
783
            );
784
        }
785
786
        $name = $this->checkName($name);
787
788
        if ($this->childExists($name)) {
789
            if (NodeInterface::CONFLICT_NOACTION === $conflict) {
790
                throw new Exception\Conflict(
791
                    'a node called '.$name.' does already exists in this collection',
792
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
793
                );
794
            }
795
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
796
                $name = $this->getDuplicateName($name, $type);
797
            }
798
        }
799
800
        return $name;
801
    }
802
803
    /**
804
     * Validate filtered collection query.
805
     */
806
    protected function validateFilter(string $filter): bool
807
    {
808
        $filter = toPHP(fromJSON($filter), [
809
            'root' => 'array',
810
            'document' => 'array',
811
            'array' => 'array',
812
        ]);
813
814
        $this->_db->storage->findOne($filter);
815
816
        return true;
817
    }
818
819
    /**
820
     * Validate acl.
821
     */
822
    protected function validateAcl(array $acl): bool
823
    {
824
        if (!$this->_acl->isAllowed($this, 'm')) {
825
            throw new ForbiddenException(
826
                'not allowed to set acl',
827
                ForbiddenException::NOT_ALLOWED_TO_MANAGE
828
            );
829
        }
830
831
        if (!$this->isSpecial()) {
832
            throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED);
833
        }
834
835
        $this->_acl->validateAcl($this->_server, $acl);
836
837
        return true;
838
    }
839
840
    /**
841
     * Get children query filter.
842
     *
843
     * Deleted:
844
     *  0 - Exclude deleted
845
     *  1 - Only deleted
846
     *  2 - Include deleted
847
     */
848
    protected function getChildrenFilter(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
849
    {
850
        $search = $filter;
851
        $search['parent'] = $this->getRealId();
852
853
        if (NodeInterface::DELETED_EXCLUDE === $deleted) {
854
            $search['deleted'] = false;
855
        } elseif (NodeInterface::DELETED_ONLY === $deleted) {
856
            $search['deleted'] = ['$type' => 9];
857
        }
858
859
        if ($this->shared) {
860
            $search = [
861
                '$and' => [
862
                    $search,
863
                    [
864
                        '$or' => [
865
                            ['shared' => $this->reference],
866
                            ['shared' => $this->shared],
867
                            ['shared' => $this->_id],
868
                        ],
869
                    ],
870
                    [
871
                        '$or' => [
872
                            [
873
                                'acl' => ['$exists' => false],
874
                            ], [
875
                                'acl.id' => (string) $this->_user->getId(),
876
                                'acl.privilege' => ['$in' => ['m', 'rw', 'r', 'w', 'w+']],
877
                            ],
878
                        ],
879
                    ],
880
                ],
881
            ];
882
        } elseif (null !== $this->_user) {
883
            $search['owner'] = $this->_user->getId();
884
        }
885
886
        if ($this->filter !== null && $this->_user !== null) {
887
            $stored = toPHP(fromJSON($this->filter), [
888
                'root' => 'array',
889
                'document' => 'array',
890
                'array' => 'array',
891
            ]);
892
893
            $include = isset($search['deleted']) ? ['deleted' => $search['deleted']] : [];
894
            $stored_filter = ['$and' => [
895
                array_merge(
896
                    $include,
897
                    $stored,
898
                    $filter
899
                ),
900
                ['$or' => [
901
                    ['owner' => $this->_user->getId()],
902
                    ['shared' => ['$in' => $this->_user->getShares()]],
903
                ]],
904
                [
905
                    '_id' => ['$ne' => $this->_id],
906
                ],
907
            ]];
908
909
            $search = ['$or' => [
910
                $search,
911
                $stored_filter,
912
            ]];
913
        }
914
915
        return $search;
916
    }
917
918
    /**
919
     * Completely remove node.
920
     */
921
    protected function _forceDelete(?string $recursion = null, bool $recursion_first = true): bool
922
    {
923
        if (!$this->isReference() && !$this->isMounted() && !$this->isFiltered()) {
924
            $this->doRecursiveAction(function ($node) use ($recursion) {
925
                $node->delete(true, $recursion, false);
926
            }, NodeInterface::DELETED_INCLUDE);
927
        }
928
929
        try {
930
            $this->_parent->getStorage()->forceDeleteCollection($this);
931
            $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...
932
933
            if ($this->isShared()) {
934
                $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...
935
            }
936
937
            $this->_logger->info('force removed collection ['.$this->_id.']', [
938
                'category' => get_class($this),
939
            ]);
940
941
            $this->_hook->run(
942
                'postDeleteCollection',
943
                [$this, true, $recursion, $recursion_first]
944
            );
945
        } catch (\Exception $e) {
946
            $this->_logger->error('failed force remove collection ['.$this->_id.']', [
947
                'category' => get_class($this),
948
                'exception' => $e,
949
            ]);
950
951
            throw $e;
952
        }
953
954
        return true;
955
    }
956
}
957