Completed
Branch dev (276354)
by Raffael
15:43
created

Collection::getAcl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Hook;
19
use Balloon\Server\User;
20
use Generator;
21
use MongoDB\BSON\ObjectId;
22
use MongoDB\BSON\Regex;
23
use MongoDB\BSON\UTCDateTime;
24
use Psr\Log\LoggerInterface;
25
use Sabre\DAV\IQuota;
26
27
class Collection extends AbstractNode implements IQuota
28
{
29
    /**
30
     * Root folder.
31
     */
32
    const ROOT_FOLDER = '/';
33
34
    /**
35
     * Share acl.
36
     *
37
     * @var array
38
     */
39
    protected $acl = [];
40
41
    /**
42
     * Share name.
43
     *
44
     * @var string
45
     */
46
    protected $share_name;
47
48
    /**
49
     * filter.
50
     *
51
     * @param string
52
     */
53
    protected $filter;
54
55
    /**
56
     * Initialize.
57
     *
58
     * @param array      $attributes
59
     * @param Filesystem $fs
60
     */
61
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl)
62
    {
63
        $this->_fs = $fs;
64
        $this->_server = $fs->getServer();
65
        $this->_db = $fs->getDatabase();
66
        $this->_user = $fs->getUser();
67
        $this->_logger = $logger;
68
        $this->_hook = $hook;
69
        $this->_acl = $acl;
70
71
        foreach ($attributes as $attr => $value) {
72
            $this->{$attr} = $value;
73
        }
74
75
        $this->mime = 'inode/directory';
76
        $this->raw_attributes = $attributes;
77
    }
78
79
    /**
80
     * Copy node with children.
81
     *
82
     * @param Collection $parent
83
     * @param int        $conflict
84
     * @param string     $recursion
85
     * @param bool       $recursion_first
86
     *
87
     * @return NodeInterface
88
     */
89
    public function copyTo(self $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
90
    {
91
        if (null === $recursion) {
92
            $recursion_first = true;
93
            $recursion = uniqid();
94
        } else {
95
            $recursion_first = false;
96
        }
97
98
        $this->_hook->run(
99
            'preCopyCollection',
100
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
101
        );
102
103
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
104
            $name = $this->getDuplicateName();
105
        } else {
106
            $name = $this->name;
107
        }
108
109
        if ($this->_id === $parent->getId()) {
110
            throw new Exception\Conflict(
111
                'can not copy node into itself',
112
                Exception\Conflict::CANT_COPY_INTO_ITSELF
113
            );
114
        }
115
116
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
117
            $new_parent = $parent->getChild($this->name);
118
        } else {
119
            $new_parent = $parent->addDirectory($name, [
120
                'created' => $this->created,
121
                'changed' => $this->changed,
122
                'deleted' => $this->deleted,
123
                'filter' => $this->filter,
124
                'meta' => $this->meta,
125
            ], NodeInterface::CONFLICT_NOACTION, true);
126
        }
127
128
        foreach ($this->getChildNodes(NodeInterface::DELETED_INCLUDE) as $child) {
129
            $child->copyTo($new_parent, $conflict, $recursion, false);
130
        }
131
132
        $this->_hook->run(
133
            'postCopyCollection',
134
            [$this, $parent, $new_parent, $conflict, $recursion, $recursion_first]
135
        );
136
137
        return $new_parent;
138
    }
139
140
    /**
141
     * Get share.
142
     *
143
     * @return array
144
     */
145
    public function getAcl(): array
146
    {
147
        if ($this->isReference()) {
148
            $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...
149
        } elseif ($this->isShare()) {
150
            $acl = $this->acl;
151
        } else {
152
            return [];
153
        }
154
155
        return $this->_acl->resolveAclTable($this->_server, $acl);
156
    }
157
158
    /**
159
     * Get Share name.
160
     *
161
     * @return string
162
     */
163
    public function getShareName(): string
164
    {
165
        if ($this->isShare()) {
166
            return $this->share_name;
167
        }
168
169
        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...
170
    }
171
172
    /**
173
     * Get Attributes.
174
     *
175
     * @return array
176
     */
177
    public function getAttributes(): array
178
    {
179
        return [
180
            '_id' => $this->_id,
181
            'name' => $this->name,
182
            'shared' => $this->shared,
183
            'share_name' => $this->share_name,
184
            'acl' => $this->acl,
185
            'directory' => true,
186
            'reference' => $this->reference,
187
            'parent' => $this->parent,
188
            'app' => $this->app,
189
            'owner' => $this->owner,
190
            'meta' => $this->meta,
191
            'mime' => $this->mime,
192
            'filter' => $this->filter,
193
            'deleted' => $this->deleted,
194
            'changed' => $this->changed,
195
            'created' => $this->created,
196
            'destroy' => $this->destroy,
197
            'readonly' => $this->readonly,
198
        ];
199
    }
200
201
    /**
202
     * Get collection.
203
     */
204
    public function get(): void
205
    {
206
        $this->getZip();
207
    }
208
209
    /**
210
     * Fetch children items of this collection.
211
     *
212
     * Deleted:
213
     *  0 - Exclude deleted
214
     *  1 - Only deleted
215
     *  2 - Include deleted
216
     *
217
     * @param int   $deleted
218
     * @param array $filter
219
     * @param int   $offset
220
     * @param int   $limit
221
     *
222
     * @return Generator
223
     */
224
    public function getChildNodes(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = [], ?int $offset = null, ?int $limit = null): Generator
225
    {
226
        $filter = $this->getChildrenFilter($deleted, $filter);
227
228
        return $this->_fs->findNodesByFilter($filter, $offset, $limit);
229
    }
230
231
    /**
232
     * Fetch children items of this collection (as array).
233
     *
234
     * Deleted:
235
     *  0 - Exclude deleted
236
     *  1 - Only deleted
237
     *  2 - Include deleted
238
     *
239
     * @param int   $deleted
240
     * @param array $filter
241
     *
242
     * @return array
243
     */
244
    public function getChildren(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
245
    {
246
        return iterator_to_array($this->getChildNodes($deleted, $filter));
247
    }
248
249
    /**
250
     * Is custom filter node.
251
     *
252
     * @return bool
253
     */
254
    public function isCustomFilter(): bool
255
    {
256
        return !empty($this->filter);
257
    }
258
259
    /**
260
     * Get number of children.
261
     *
262
     * @return int
263
     */
264
    public function getSize(): int
265
    {
266
        return $this->_db->storage->count($this->getChildrenFilter());
267
    }
268
269
    /**
270
     * Get real id (reference).
271
     *
272
     * @return ObjectId
273
     */
274
    public function getRealId(): ?ObjectId
275
    {
276
        if (true === $this->shared && $this->isReference()) {
277
            return $this->reference;
278
        }
279
280
        return $this->_id;
281
    }
282
283
    /**
284
     * Get user quota information.
285
     *
286
     * @return array
287
     */
288
    public function getQuotaInfo(): array
289
    {
290
        $quota = $this->_user->getQuotaUsage();
291
292
        return [
293
            $quota['used'],
294
            $quota['available'],
295
        ];
296
    }
297
298
    /**
299
     * Fetch children items of this collection.
300
     *
301
     * @param string $node
0 ignored issues
show
Bug introduced by
There is no parameter named $node. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
302
     * @param int    $deleted
303
     * @param array  $filter
304
     * @param mixed  $name
305
     *
306
     * @return NodeInterface
307
     */
308
    public function getChild($name, int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): NodeInterface
309
    {
310
        $filter = $this->getChildrenFilter($deleted, $filter);
311
        $filter['name'] = new Regex('^'.preg_quote($name).'$', 'i');
312
        $node = $this->_db->storage->findOne($filter);
313
314
        if (null === $node) {
315
            throw new Exception\NotFound(
316
                'node called '.$name.' does not exists here',
317
                Exception\NotFound::NODE_NOT_FOUND
318
            );
319
        }
320
321
        $this->_logger->debug('loaded node ['.$node['_id'].' from parent node ['.$this->getRealId().']', [
322
            'category' => get_class($this),
323
        ]);
324
325
        return $this->_fs->initNode($node);
326
    }
327
328
    /**
329
     * Delete node.
330
     *
331
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
332
     * delete finally
333
     *
334
     * @param bool   $force
335
     * @param string $recursion       Identifier to identify a recursive action
336
     * @param bool   $recursion_first
337
     *
338
     * @return bool
339
     */
340
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
341
    {
342
        if (!$this->isReference() && !$this->_acl->isAllowed($this, 'w')) {
343
            throw new ForbiddenException(
344
                'not allowed to delete node '.$this->name,
345
                ForbiddenException::NOT_ALLOWED_TO_DELETE
346
            );
347
        }
348
349
        if (null === $recursion) {
350
            $recursion_first = true;
351
            $recursion = uniqid();
352
        } else {
353
            $recursion_first = false;
354
        }
355
356
        $this->_hook->run(
357
            'preDeleteCollection',
358
            [$this, &$force, &$recursion, &$recursion_first]
359
        );
360
361
        if ($this->readonly && null !== $this->_user) {
362
            throw new Exception\Conflict(
363
                'node is marked as readonly, it is not possible to delete it',
364
                Exception\Conflict::READONLY
365
            );
366
        }
367
368
        if (true === $force) {
369
            return $this->_forceDelete($recursion, $recursion_first);
370
        }
371
372
        $this->deleted = new UTCDateTime();
373
374
        if (!$this->isReference()) {
375
            $this->doRecursiveAction(function ($node) use ($recursion) {
376
                $node->delete(false, $recursion, false);
377
            }, NodeInterface::DELETED_EXCLUDE);
378
        }
379
380
        if (null !== $this->_id) {
381
            $result = $this->save([
382
                'deleted',
383
            ], [], $recursion, false);
384
        } else {
385
            $result = true;
386
        }
387
388
        $this->_hook->run(
389
            'postDeleteCollection',
390
            [$this, $force, $recursion, $recursion_first]
391
        );
392
393
        return $result;
394
    }
395
396
    /**
397
     * Check if this collection has child named $name.
398
     *
399
     * deleted:
400
     *
401
     *  0 - Exclude deleted
402
     *  1 - Only deleted
403
     *  2 - Include deleted
404
     *
405
     * @param string $name
406
     * @param int    $deleted
407
     * @param array  $filter
408
     *
409
     * @return bool
410
     */
411
    public function childExists($name, $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): bool
412
    {
413
        $find = [
414
            'parent' => $this->getRealId(),
415
            'name' => new Regex('^'.preg_quote($name).'$', 'i'),
416
        ];
417
418
        if (null !== $this->_user) {
419
            $find['owner'] = $this->_user->getId();
420
        }
421
422
        switch ($deleted) {
423
            case NodeInterface::DELETED_EXCLUDE:
424
                $find['deleted'] = false;
425
426
                break;
427
            case NodeInterface::DELETED_ONLY:
428
                $find['deleted'] = ['$type' => 9];
429
430
                break;
431
        }
432
433
        $find = array_merge($filter, $find);
434
435
        if ($this->isSpecial()) {
436
            unset($find['owner']);
437
        }
438
439
        $node = $this->_db->storage->findOne($find);
440
441
        return (bool) $node;
442
    }
443
444
    /**
445
     * Share collection.
446
     *
447
     * @param array  $acl
448
     * @param string $name
449
     *
450
     * @return bool
451
     */
452
    public function share(array $acl, string $name): bool
453
    {
454
        if ($this->isShareMember()) {
455
            throw new Exception('a sub node of a share can not be shared');
456
        }
457
458
        if (!$this->_acl->isAllowed($this, 'm')) {
459
            throw new ForbiddenException(
460
                'not allowed to share node',
461
                ForbiddenException::NOT_ALLOWED_TO_SHARE
462
            );
463
        }
464
465
        $this->_acl->validateAcl($this->_server, $acl);
466
467
        $action = [
468
            '$set' => [
469
                'shared' => $this->getRealId(),
470
            ],
471
        ];
472
473
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares, $files);
474
475
        if (!empty($shares)) {
476
            throw new Exception('child folder contains a shared folder');
477
        }
478
479
        $this->_db->storage->updateMany([
480
            '_id' => [
481
                '$in' => $toset,
482
            ],
483
        ], $action);
484
485
        $this->_db->delta->updateMany([
486
            '_id' => [
487
                '$in' => $toset,
488
            ],
489
        ], $action);
490
491
        if ($this->getRealId() === $this->_id) {
492
            $this->acl = $acl;
493
            $this->shared = true;
494
            $this->share_name = $name;
495
            $this->save(['acl', 'shared', 'share_name']);
496
        } else {
497
            $this->_db->storage->updateOne([
498
                '_id' => $this->getRealId(),
499
            ], [
500
                '$set' => [
501
                    'share_name' => $name,
502
                    'acl' => $acl,
503
                ],
504
            ]);
505
        }
506
507
        if (!is_array($files)) {
508
            return true;
509
        }
510
511
        foreach ($files as $node) {
512
            if (null === $node['storage']) {
513
                continue;
514
            }
515
516
            $share_ref = [
517
                'id' => $node['id'],
518
                'share' => $this->_id,
519
            ];
520
521
            $this->_db->{'fs.files'}->updateOne(
522
                $node['storage'],
523
                [
524
                    '$addToSet' => [
525
                        'metadata.share_ref' => $share_ref,
526
                    ],
527
                ]
528
            );
529
        }
530
531
        return true;
532
    }
533
534
    /**
535
     * Unshare collection.
536
     *
537
     * @return bool
538
     */
539
    public function unshare(): bool
540
    {
541
        if (!$this->_acl->isAllowed($this, 'm')) {
542
            throw new ForbiddenException(
543
                'not allowed to share node',
544
                ForbiddenException::NOT_ALLOWED_TO_SHARE
545
            );
546
        }
547
548
        if (true !== $this->shared) {
549
            throw new Exception\Conflict(
550
                'Can not unshare a none shared collection',
551
                Exception\Conflict::NOT_SHARED
552
            );
553
        }
554
555
        $this->shared = false;
556
        $this->share_name = null;
557
        $this->acl = [];
558
        $action = [
559
            '$unset' => [
560
                'shared' => $this->_id,
561
            ],
562
            '$set' => [
563
                'owner' => $this->_user->getId(),
564
            ],
565
        ];
566
567
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares, $files);
568
569
        $this->_db->storage->updateMany([
570
            '_id' => [
571
                '$in' => $toset,
572
            ],
573
        ], $action);
574
575
        $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...
576
577
        if (!is_array($files)) {
578
            return true;
579
        }
580
581
        foreach ($files as $node) {
582
            if (null === $node['storage']) {
583
                continue;
584
            }
585
586
            $share_ref = [
587
                'id' => $node['id'],
588
                'share' => $this->_id,
589
            ];
590
591
            $this->_db->{'fs.files'}->updateOne(
592
                $node['storage']['attributes'],
593
                [
594
                '$pull' => [
595
                    'metadata.share_ref' => $share_ref,
596
                    ],
597
                ]
598
            );
599
        }
600
601
        return true;
602
    }
603
604
    /**
605
     * Get children.
606
     *
607
     * @param ObjectId $id
608
     * @param array    $shares
609
     * @param arary    $files
610
     *
611
     * @return array
612
     */
613
    public function getChildrenRecursive(?ObjectId $id = null, ?array &$shares = [], ?array &$files = []): array
614
    {
615
        $list = [];
616
        $result = $this->_db->storage->find([
617
            'parent' => $id,
618
        ], [
619
            '_id' => 1,
620
            'directory' => 1,
621
            'reference' => 1,
622
            'shared' => 1,
623
            'storage' => 1,
624
        ]);
625
626
        foreach ($result as $node) {
627
            $list[] = $node['_id'];
628
629
            if (false === $node['directory']) {
630
                $files[] = [
631
                    'id' => $node['_id'],
632
                    'storage' => isset($node['storage']) ? $node['storage'] : null,
633
                ];
634
            } else {
635
                if (isset($node['reference']) || isset($node['shared']) && true === $node['shared']) {
636
                    $shares[] = $node['_id'];
637
                }
638
639
                if (true === $node['directory'] && !isset($node['reference'])) {
640
                    $list = array_merge($list, $this->getChildrenRecursive($node['_id'], $shares, $files));
641
                }
642
            }
643
        }
644
645
        return $list;
646
    }
647
648
    /**
649
     * Create new directory.
650
     *
651
     * @param string $name
652
     * @param arracy $attributes
653
     * @param int    $conflict
654
     * @param bool   $clone
655
     *
656
     * @return Collection
657
     */
658
    public function addDirectory($name, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): self
659
    {
660
        if (!$this->_acl->isAllowed($this, 'w')) {
661
            throw new ForbiddenException(
662
                'not allowed to create new node here',
663
                ForbiddenException::NOT_ALLOWED_TO_CREATE
664
            );
665
        }
666
667
        $this->_hook->run('preCreateCollection', [$this, &$name, &$attributes, &$clone]);
668
669
        if ($this->readonly) {
670
            throw new Exception\Conflict(
671
                'node is set as readonly, it is not possible to add new sub nodes',
672
                Exception\Conflict::READONLY
673
            );
674
        }
675
676
        $name = $this->checkName($name);
677
678
        if ($this->childExists($name)) {
679
            if (NodeInterface::CONFLICT_NOACTION === $conflict) {
680
                throw new Exception\Conflict(
681
                    'a node called '.$name.' does already exists in this collection',
682
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
683
                );
684
            }
685
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
686
                $name = $this->getDuplicateName($name);
687
            }
688
        }
689
690
        if ($this->isDeleted()) {
691
            throw new Exception\Conflict(
692
                'could not add node '.$name.' into a deleted parent collection',
693
                Exception\Conflict::DELETED_PARENT
694
            );
695
        }
696
697
        try {
698
            $meta = [
699
                'name' => $name,
700
                'deleted' => false,
701
                'parent' => $this->getRealId(),
702
                'directory' => true,
703
                'created' => new UTCDateTime(),
704
                'changed' => new UTCDateTime(),
705
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
706
            ];
707
708
            if (null !== $this->_user) {
709
                $meta['owner'] = $this->_user->getId();
710
            }
711
712
            $save = array_merge($meta, $attributes);
713
714
            $result = $this->_db->storage->insertOne($save, [
715
                '$isolated' => true,
716
            ]);
717
718
            $save['_id'] = $result->getInsertedId();
719
720
            $this->_logger->info('added new collection ['.$save['_id'].'] under parent ['.$this->_id.']', [
721
                'category' => get_class($this),
722
            ]);
723
724
            $this->changed = $save['changed'];
725
            $this->save('changed');
726
727
            $new = $this->_fs->initNode($save);
728
            $this->_hook->run('postCreateCollection', [$this, $new, $clone]);
729
730
            return $new;
731
        } catch (\Exception $e) {
732
            $this->_logger->error('failed create new collection under parent ['.$this->_id.']', [
733
                'category' => get_class($this),
734
                'exception' => $e,
735
            ]);
736
737
            throw $e;
738
        }
739
    }
740
741
    /**
742
     * Create new file as a child from this collection.
743
     *
744
     * @param string $name
745
     * @param mixed  $data
746
     * @param array  $attributes
747
     * @param int    $conflict
748
     * @param bool   $clone
749
     *
750
     * @return File
751
     */
752
    public function addFile($name, $data = null, array $attributes = [], int $conflict = NodeInterface::CONFLICT_NOACTION, bool $clone = false): File
753
    {
754
        if (!$this->_acl->isAllowed($this, 'w')) {
755
            throw new ForbiddenException(
756
                'not allowed to create new node here',
757
                ForbiddenException::NOT_ALLOWED_TO_CREATE
758
            );
759
        }
760
761
        $this->_hook->run('preCreateFile', [$this, &$name, &$attributes, &$clone]);
762
763
        if ($this->readonly) {
764
            throw new Exception\Conflict(
765
                'node is set as readonly, it is not possible to add new sub nodes',
766
                Exception\Conflict::READONLY
767
            );
768
        }
769
770
        $name = $this->checkName($name);
771
772
        if ($this->childExists($name)) {
773
            if (NodeInterface::CONFLICT_NOACTION === $conflict) {
774
                throw new Exception\Conflict(
775
                    'a node called '.$name.' does already exists in this collection',
776
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
777
                );
778
            }
779
            if (NodeInterface::CONFLICT_RENAME === $conflict) {
780
                $name = $this->getDuplicateName($name);
781
            }
782
        }
783
784
        if ($this->isDeleted()) {
785
            throw new Exception\Conflict(
786
                'could not add node '.$name.' into a deleted parent collection',
787
                Exception\Conflict::DELETED_PARENT
788
            );
789
        }
790
791
        try {
792
            $meta = [
793
                'name' => $name,
794
                'deleted' => false,
795
                'parent' => $this->getRealId(),
796
                'directory' => false,
797
                'hash' => null,
798
                'created' => new UTCDateTime(),
799
                'changed' => new UTCDateTime(),
800
                'version' => 0,
801
                'shared' => (true === $this->shared ? $this->getRealId() : $this->shared),
802
                'storage_adapter' => $this->storage_adapter,
803
            ];
804
805
            if (null !== $this->_user) {
806
                $meta['owner'] = $this->_user->getId();
807
            }
808
809
            $save = array_merge($meta, $attributes);
810
811
            $result = $this->_db->storage->insertOne($save, [
812
                '$isolated' => true,
813
            ]);
814
815
            $save['_id'] = $result->getInsertedId();
816
817
            $this->_logger->info('added new file ['.$save['_id'].'] under parent ['.$this->_id.']', [
818
                'category' => get_class($this),
819
            ]);
820
821
            $this->changed = $save['changed'];
822
            $this->save('changed');
823
824
            $file = $this->_fs->initNode($save);
825
826
            try {
827
                $file->put($data, true, $attributes);
828
            } catch (Exception\InsufficientStorage $e) {
829
                $this->_logger->warning('failed add new file under parent ['.$this->_id.'], cause user quota is full', [
830
                    'category' => get_class($this),
831
                    'exception' => $e,
832
                ]);
833
834
                $this->_db->storage->deleteOne([
835
                    '_id' => $save['_id'],
836
                ]);
837
838
                throw $e;
839
            }
840
841
            $this->_hook->run('postCreateFile', [$this, $file, $clone]);
842
843
            return $file;
844
        } catch (\Exception $e) {
845
            $this->_logger->error('failed add new file under parent ['.$this->_id.']', [
846
                'category' => get_class($this),
847
                'exception' => $e,
848
            ]);
849
850
            throw $e;
851
        }
852
    }
853
854
    /**
855
     * Create new file wrapper
856
     * (Sabe\DAV compatible method, elsewhere use addFile().
857
     *
858
     * Sabre\DAV requires that createFile() returns the ETag instead the newly created file instance
859
     *
860
     * @param string $name
861
     * @param string $data
862
     *
863
     * @return File
864
     */
865
    public function createFile($name, $data = null): String
866
    {
867
        return $this->addFile($name, $data)->getETag();
868
    }
869
870
    /**
871
     * Create new directory wrapper
872
     * (Sabe\DAV compatible method, elsewhere use addDirectory().
873
     *
874
     * Sabre\DAV requires that createDirectory() returns void
875
     *
876
     * @param string $name
877
     */
878
    public function createDirectory($name): void
879
    {
880
        $this->addDirectory($name);
881
    }
882
883
    /**
884
     * Do recursive Action.
885
     *
886
     * @param callable $callable
887
     * @param int      $deleted
888
     * @param bool     $ignore_exception
0 ignored issues
show
Bug introduced by
There is no parameter named $ignore_exception. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
889
     *
890
     * @return bool
891
     */
892
    public function doRecursiveAction(callable $callable, int $deleted = NodeInterface::DELETED_EXCLUDE): bool
893
    {
894
        $children = $this->getChildNodes($deleted, []);
895
896
        foreach ($children as $child) {
897
            $callable($child);
898
        }
899
900
        return true;
901
    }
902
903
    /**
904
     * Get children query filter.
905
     *
906
     * Deleted:
907
     *  0 - Exclude deleted
908
     *  1 - Only deleted
909
     *  2 - Include deleted
910
     *
911
     * @param int   $deleted
912
     * @param array $filter
913
     *
914
     * @return array
915
     */
916
    protected function getChildrenFilter(int $deleted = NodeInterface::DELETED_EXCLUDE, array $filter = []): array
917
    {
918
        $search = [
919
            'parent' => $this->getRealId(),
920
        ];
921
922
        if (NodeInterface::DELETED_EXCLUDE === $deleted) {
923
            $search['deleted'] = false;
924
        } elseif (NodeInterface::DELETED_ONLY === $deleted) {
925
            $search['deleted'] = ['$type' => 9];
926
        }
927
928
        $search = array_merge($filter, $search);
929
930
        if ($this->shared) {
931
            $search = [
932
                '$and' => [
933
                    $search,
934
                    [
935
                        '$or' => [
936
                            ['shared' => $this->reference],
937
                            ['shared' => $this->shared],
938
                            ['shared' => $this->_id],
939
                        ],
940
                    ],
941
                ],
942
            ];
943
        } elseif (null !== $this->_user) {
944
            $search['owner'] = $this->_user->getId();
945
        }
946
947
        if ($this->filter !== null) {
948
            $stored_filter = ['$and' => [
949
                array_merge(
950
                    ['deleted' => $search['deleted']],
951
                    json_decode($this->filter),
952
                    $filter
953
                ),
954
                ['$or' => [
955
                    ['owner' => $this->_user->getId()],
956
                    ['shared' => ['$in' => $this->_user->getShares()]],
957
                ]],
958
            ]];
959
960
            $search = ['$or' => [
961
                $search,
962
                $stored_filter,
963
            ]];
964
        }
965
966
        return $search;
967
    }
968
969
    /**
970
     * Completely remove node.
971
     *
972
     * @param string $recursion       Identifier to identify a recursive action
973
     * @param bool   $recursion_first
974
     *
975
     * @return bool
976
     */
977
    protected function _forceDelete(?string $recursion = null, bool $recursion_first = true): bool
978
    {
979
        if (!$this->isReference()) {
980
            $this->doRecursiveAction(function ($node) use ($recursion) {
981
                $node->delete(true, $recursion, false);
982
            }, NodeInterface::DELETED_INCLUDE);
983
        }
984
985
        try {
986
            $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...
987
988
            if ($this->isShared()) {
989
                $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...
990
            }
991
992
            $this->_logger->info('force removed collection ['.$this->_id.']', [
993
                'category' => get_class($this),
994
            ]);
995
996
            $this->_hook->run(
997
                'postDeleteCollection',
998
                [$this, true, $recursion, $recursion_first]
999
            );
1000
        } catch (\Exception $e) {
1001
            $this->_logger->error('failed force remove collection ['.$this->_id.']', [
1002
                'category' => get_class($this),
1003
                'exception' => $e,
1004
            ]);
1005
1006
            throw $e;
1007
        }
1008
1009
        return true;
1010
    }
1011
}
1012