Completed
Push — master ( d4fe43...84d749 )
by Raffael
02:51
created

Collection::createFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * Balloon
6
 *
7
 * @author      Raffael Sahli <[email protected]>
8
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPLv3 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use \Sabre\DAV;
15
use Balloon\Exception;
16
use Balloon\Helper;
17
use Balloon\User;
18
use Balloon\Resource;
19
use \Psr\Log\LoggerInterface as Logger;
20
use Balloon\Plugin;
21
use Balloon\Filesystem;
22
use \MongoDB\BSON\ObjectId;
23
use \MongoDB\BSON\UTCDateTime;
24
use \MongoDB\Model\BSONDocument;
25
use \MongoDB\BSON\Regex;
26
use \Generator;
27
28
class Collection extends Node implements INode, DAV\ICollection, DAV\IQuota
29
{
30
    /**
31
     * Root folder
32
     */
33
    const ROOT_FOLDER = '/';
34
35
36
    /**
37
     * Mime type
38
     *
39
     * @var string
40
     */
41
    protected $mime = 'inode/directory';
42
43
    
44
    /**
45
     * Children
46
     *
47
     * @var array
48
     */
49
    protected $children = [];
50
51
52
    /**
53
     * Share acl
54
     *
55
     * @var array
56
     */
57
    protected $acl;
58
59
60
    /**
61
     * filter
62
     *
63
     * @param array
64
     */
65
    protected $filter = [];
66
67
68
    /**
69
     * Initialize
70
     *
71
     * @param  BSONDocument $node
72
     * @param  Filesystem $fs
73
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
74
     */
75
    public function __construct(?BSONDocument $node, Filesystem $fs)
76
    {
77
        parent::__construct($node, $fs);
78
        
79
        if ($node === null) {
80
            $this->_id = null;
81
82
            if ($this->_user instanceof User) {
83
                $this->owner  = $this->_user->getId();
84
            }
85
        }
86
            
87
        $this->_verifyAccess();
88
    }
89
90
91
    /**
92
     * Copy node with children
93
     *
94
     * @param  Collection $parent
95
     * @param  int $conflict
96
     * @param  string $recursion
97
     * @param  bool $recursion_first
98
     * @return INode
99
     */
100
    public function copyTo(Collection $parent, int $conflict=INode::CONFLICT_NOACTION, ?string $recursion=null, bool $recursion_first=true): INode
101
    {
102
        if ($recursion === null) {
103
            $recursion_first = true;
104
            $recursion = uniqid();
105
        } else {
106
            $recursion_first = false;
107
        }
108
109
        $this->_pluginmgr->run('preCopyCollection',
110
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
111
        );
112
113
        if ($conflict === INode::CONFLICT_RENAME && $parent->childExists($this->name)) {
114
            $name = $this->_getDuplicateName();
115
        } else {
116
            $name = $this->name;
117
        }
118
        
119
        if ($this->_id == $parent->getId()) {
120
            throw new Exception\Conflict('can not copy node into itself',
121
                Exception\Conflict::CANT_COPY_INTO_ITSELF
122
            );
123
        }
124
125
        if ($conflict === INode::CONFLICT_MERGE && $parent->childExists($this->name)) {
126
            $new_parent = $parent->getChild($this->name);
127
        } else {
128
            $new_parent = $parent->addDirectory($name, [
129
                'created' => $this->created,
130
                'changed' => $this->changed,
131
                'deleted' => $this->deleted,
132
                'filter'  => $this->filter,
133
            ], INode::CONFLICT_NOACTION, true);
134
        }
135
136
        foreach ($this->getChildNodes(INode::DELETED_INCLUDE) as $child) {
137
            $child->copyTo($new_parent, $conflict, $recursion, false);
138
        }
139
140
        $this->_pluginmgr->run('postCopyCollection',
141
            [$this, $parent, $new_parent, $conflict, $recursion, $recursion_first]
142
        );
143
144
        return $new_parent;
145
    }
146
147
148
    /**
149
     * Get share
150
     *
151
     * @return array|bool
152
     */
153
    public function getShare()
154
    {
155
        if (!$this->isShared()) {
156
            return false;
157
        } else {
158
            if (is_array($this->acl)) {
159
                $resource = new Resource($this->_user, $this->_logger, $this->_fs);
160
                $return = [];
161
                if (array_key_exists('user', $this->acl)) {
162
                    foreach ((array)$this->acl['user'] as $user) {
163
                        $data = $resource->searchUser($user['user'], true);
164
165
                        if (empty($data)) {
166
                            continue;
167
                        }
168
169
                        $data['priv'] = $user['priv'];
170
                        $return[] = $data;
171
                    }
172
                }
173
                if (array_key_exists('group', $this->acl)) {
174
                    foreach ((array)$this->acl['group'] as $group) {
175
                        $data = $resource->searchGroup($group['group'], true);
176
177
                        if (empty($data)) {
178
                            continue;
179
                        }
180
181
                        $data['priv'] = $group['priv'];
182
                        $return[] = $data;
183
                    }
184
                }
185
                return $return;
186
            } else {
187
                return false;
188
            }
189
        }
190
    }
191
192
193
    /**
194
     * Get Attribute
195
     *
196
     * @param  array|string $attribute
197
     * @return array|string
198
     */
199
    public function getAttribute($attribute=[])
200
    {
201
        if (empty($attribute)) {
202
            $attribute = [
203
                'id',
204
                'name',
205
                'meta',
206
                'mime',
207
                'size',
208
                'sharelink',
209
                'reference',
210
                'deleted',
211
                'changed',
212
                'created',
213
                'share',
214
                'directory'
215
            ];
216
        }
217
    
218
        return $this->_getAttribute($attribute);
219
    }
220
221
    
222
    /**
223
     * Get collection
224
     *
225
     * @return void
226
     */
227
    public function get(): void
228
    {
229
        $this->getZip();
230
    }
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
     * @param   int $deleted
242
     * @param   array $filter
243
     * @return  Generator
244
     */
245
    public function getChildNodes(int $deleted=INode::DELETED_EXCLUDE, array $filter=[]): Generator
246
    {
247
        if ($this->_user instanceof User) {
248
            $this->_user->findNewShares();
249
        }
250
251
        $search = [
252
            'parent' => $this->getRealId(),
253
        ];
254
255
        if ($deleted === INode::DELETED_EXCLUDE) {
256
            $search['deleted'] = false;
257
        } elseif ($deleted === INode::DELETED_ONLY) {
258
            $search['deleted'] = ['$type' => 9];
259
        }
260
261
        $search = array_merge($filter, $search);
262
263
        if ($this->shared) {
264
            $node = $this->_db->storage->find([
265
                '$and' => [
266
                    $search,
267
                    [
268
                        '$or' => [
269
                            ['shared' => $this->reference],
270
                            ['shared' => $this->shared],
271
                            ['shared' => $this->_id],
272
                        ]
273
                    ]
274
                ]
275
            ]);
276
        } else {
277
            if ($this->_user !== null) {
278
                $search['owner'] = $this->_user->getId();
279
            }
280
            
281
            $node = $this->_db->storage->find($search);
282
        }
283
        
284
        $list = [];
0 ignored issues
show
Unused Code introduced by
$list 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...
285
        foreach ($node as $child) {
286
            try {
287
                yield $this->getChild($child);
288
            } catch (\Exception $e) {
289
                $this->_logger->info('remove node from children list, failed load node', [
290
                    'category'  => get_class($this),
291
                    'exception' => $e
292
                ]);
293
            }
294
        }
295
        
296
        if (!empty($this->filter)) {
297
            foreach ($this->_fs->findNodesWithCustomFilterUser($deleted, $this->filter) as $node) {
298
                yield $node;
299
            }
300
        }
301
    }
302
303
304
    /**
305
     * Fetch children items of this collection (as array)
306
     *
307
     * Deleted:
308
     *  0 - Exclude deleted
309
     *  1 - Only deleted
310
     *  2 - Include deleted
311
     *
312
     * @param   int $deleted
313
     * @param   array $filter
314
     * @return  Generator
315
     */
316
    public function getChildren(int $deleted=INode::DELETED_EXCLUDE, array $filter=[]): array
317
    {
318
        return iterator_to_array($this->getChildNodes($deleted, $filter));
319
    }
320
321
322
    /**
323
     * Is custom filter node
324
     *
325
     * @return bool
326
     */
327
    public function isCustomFilter(): bool
328
    {
329
        return !empty($this->filter);
330
    }
331
332
    
333
    /**
334
     * Get number of children
335
     *
336
     * @return int
337
     */
338
    public function getSize(): int
339
    {
340
        if ($this->isDeleted()) {
341
            return count(iterator_to_array($this->getChildNodes(INode::DELETED_INCLUDE)));
342
        } else {
343
            return count(iterator_to_array($this->getChildNodes()));
344
        }
345
    }
346
347
348
    /**
349
     * Get real id (reference)
350
     *
351
     * @return ObjectId
352
     */
353
    public function getRealId(): ?ObjectId
354
    {
355
        if ($this->shared == true && $this->isReference()) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
356
            return $this->reference;
357
        } else {
358
            return $this->_id;
359
        }
360
    }
361
362
363
    /**
364
     * Get user quota information
365
     *
366
     * @return array
367
     */
368
    public function getQuotaInfo(): array
369
    {
370
        $quota = $this->_user->getQuotaUsage();
371
        
372
        return [
373
            $quota['used'],
374
            $quota['available']
375
        ];
376
    }
377
378
379
    /**
380
     * Fetch children items of this collection
381
     *
382
     * @param  Collection|File|string $node
383
     * @param  int $deleted
384
     * @param  array $filter
385
     * @return INode
386
     */
387
    public function getChild($node, int $deleted=INode::DELETED_EXCLUDE, array $filter=[]): INode
388
    {
389
        //if $node is string load the object from the backend based on the current parent (the name
390
        //is unique per depth, so we can load the object)
391
        if (is_string($node)) {
392
            $name = $node;
393
            $search = [
394
                'name'    => $name,
395
                'parent'  => $this->getRealId(),
396
            ];
397
    
398
            switch ($deleted) {
399
                case INode::DELETED_EXCLUDE:
400
                    $search['deleted'] = false;
401
                    break;
402
                case INode::DELETED_ONLY:
403
                    $search['deleted'] = ['$type' => '9'];
404
                    break;
405
            }
406
407
408
            $search = array_merge($filter, $search);
409
410
            if ($this->shared) {
411
                $node = $this->_db->storage->findOne([
412
                    '$and' => [
413
                        $search,
414
                        [
415
                            '$or' => [
416
                                ['shared' => $this->reference],
417
                                ['shared' => $this->shared],
418
                                ['shared' => $this->_id]
419
                            ]
420
                        ]
421
                    ]
422
                ]);
423
            } else {
424
                $search['owner'] = $this->_user->getId();
425
                $node = $this->_db->storage->findOne($search);
426
            }
427
428
            if ($node === null) {
429
                throw new Exception\NotFound('node called '.$name.' does not exists here',
430
                    Exception\NotFound::NODE_NOT_FOUND
431
                );
432
            }
433
        }
434
435
        $this->_logger->debug('loaded node ['.$node['_id'].' (directory='.$node['directory'].')] from parent node ['.$this->getRealId().']', [
436
            'category' => get_class($this),
437
        ]);
438
        
439
        //if the item has the directory flag we create a collection else the item is file
440
        if ($node['directory'] == true) {
441
            return new Collection($node, $this->_fs);
442
        } else {
443
            return new File($node, $this->_fs);
444
        }
445
    }
446
447
448
    /**
449
     * Do recursive Action
450
     *
451
     * @param   string $method
452
     * @param   array $params
453
     * @param   int $deleted
454
     * @return  bool
455
     */
456
    protected function doRecursiveAction(string $method, array $params=[], int $deleted=INode::DELETED_EXCLUDE): bool
457
    {
458
        if (!is_callable([$this, $method])) {
459
            throw new Exception("method $method is not callable in ".__CLASS__);
460
        }
461
462
        $children = $this->getChildNodes($deleted);
463
464
        foreach ($children as $child) {
465
            call_user_func_array([$child, $method], $params);
466
        }
467
468
        return true;
469
    }
470
471
472
    /**
473
     * Delete node
474
     *
475
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
476
     * delete finally
477
     *
478
     * @param   bool $force
479
     * @param   string $recursion Identifier to identify a recursive action
480
     * @param   bool $recursion_first
481
     * @return  bool
482
     */
483
    public function delete(bool $force=false, ?string $recursion=null, bool $recursion_first=true): bool
484
    {
485
        if (!$this->isAllowed('w') && !$this->isReference()) {
486
            throw new Exception\Forbidden('not allowed to delete node '.$this->name,
487
                Exception\Forbidden::NOT_ALLOWED_TO_DELETE
488
            );
489
        }
490
491
        if ($recursion === null) {
492
            $recursion_first = true;
493
            $recursion = uniqid();
494
        } else {
495
            $recursion_first = false;
496
        }
497
498
        $this->_pluginmgr->run('preDeleteCollection',
499
            [$this, &$force, &$recursion, &$recursion_first]
500
        );
501
        
502
        if ($this->readonly && $this->_user !== null) {
503
            throw new Exception\Conflict('node is marked as readonly, it is not possible to delete it',
504
                Exception\Conflict::READONLY
505
            );
506
        }
507
        
508
        if ($force === true) {
509
            return $this->_forceDelete($recursion, $recursion_first);
510
        }
511
        
512
        $this->deleted = new UTCDateTime();
513
        
514
515
        if (!$this->isReference()) {
516
            $this->doRecursiveAction('delete', [
517
                'force'     => false,
518
                'recursion' => $recursion,
519
                'recursion_first' => false,
520
            ]);
521
        }
522
523
        if ($this->_id !== null) {
524
            $result = $this->save([
525
                'deleted',
526
            ], [], $recursion, false);
527
        } else {
528
            $result = true;
529
        }
530
531
        $this->_pluginmgr->run('postDeleteCollection',
532
            [$this, $force, $recursion, $recursion_first]
533
        );
534
535
        return $result;
536
    }
537
538
539
    /**
540
     * Completely remove node
541
     *
542
     * @param   string $recursion Identifier to identify a recursive action
543
     * @param   bool $recursion_first
544
     * @return  bool
545
     */
546
    protected function _forceDelete(?string $recursion=null, bool $recursion_first=true): bool
547
    {
548
        if (!$this->isReference()) {
549
            $this->doRecursiveAction('delete', [
550
                'force'     => true,
551
                'recursion' => $recursion,
552
                'recursion_first' => false,
553
            ], INode::DELETED_INCLUDE);
554
        }
555
  
556
        try {
557
            $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...
558
559
            if ($this->isShared()) {
560
                $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...
561
            }
562
563
            $this->_logger->info('force removed collection ['.$this->_id.']', [
564
                'category' => get_class($this),
565
            ]);
566
        
567
568
            $this->_pluginmgr->run('postDeleteCollection',
569
                [$this, true, $recursion, $recursion_first]
570
            );
571
        } catch (\Exception $e) {
572
            $this->_logger->error('failed force remove collection ['.$this->_id.']', [
573
                'category' => get_class($this),
574
                'exception' => $e,
575
            ]);
576
            
577
            throw $e;
578
        }
579
580
        return true;
581
    }
582
583
584
    /**
585
     * Check if this collection has child named $name
586
     *
587
     * deleted:
588
     *
589
     *  0 - Exclude deleted
590
     *  1 - Only deleted
591
     *  2 - Include deleted
592
     *
593
     * @param   string $name
594
     * @param   int $deleted
595
     * @param   array $filter
596
     * @return  bool
597
     */
598
    public function childExists($name, $deleted=INode::DELETED_EXCLUDE, array $filter=[]): bool
599
    {
600
        $find = [
601
            'parent'  => $this->getRealId(),
602
            'name'    => new Regex('^'.preg_quote($name).'$', 'i'),
603
        ];
604
605
        if ($this->_user !== null) {
606
            $find['owner'] = $this->_user->getId();
607
        }
608
609
        switch ($deleted) {
610
            case INode::DELETED_EXCLUDE:
611
                $find['deleted'] = false;
612
                break;
613
            case INode::DELETED_ONLY:
614
                $find['deleted'] = ['$type' => 9];
615
                break;
616
        }
617
618
        $find = array_merge($filter, $find);
619
620
        if ($this->isShared()) {
621
            unset($find['owner']);
622
        }
623
        
624
        $node = $this->_db->storage->findOne($find);
625
    
626
        return (bool)$node;
627
    }
628
    
629
630
    /**
631
     * Share collection
632
     *
633
     * @param   array $acl
634
     * @return  bool
635
     */
636
    public function share(array $acl): bool
637
    {
638
        if ($this->isReference()) {
639
            throw new Exception('a collection reference can not be shared');
640
        }
641
        
642
        if ($this->isShareMember()) {
643
            throw new Exception('a sub node of a share can not be shared');
644
        }
645
        
646
        if (!$this->isAllowed('w')) {
647
            throw new Exception\Forbidden('not allowed to share node',
648
                Exception\Forbidden::NOT_ALLOWED_TO_SHARE
649
            );
650
        }
651
652
        $this->shared = true;
653
654
        $this->acl = $acl;
655
        $action = [
656
            '$set' => [
657
                'shared' => $this->_id
658
            ]
659
        ];
660
    
661
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares, $files);
662
        
663
        if (!empty($shares)) {
664
            throw new Exception('child folder contains a shared folder');
665
        }
666
        
667
        $this->_db->storage->updateMany([
668
            '_id' => [
669
                '$in' => $toset,
670
            ],
671
        ], $action);
672
673
        $this->_db->delta->updateMany([
674
            '_id' => [
675
                '$in' => $toset,
676
            ],
677
        ], $action);
678
        
679
        $list = [];
0 ignored issues
show
Unused Code introduced by
$list 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...
680
        $result = $this->save(['shared', 'acl']);
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...
681
682
        if (!is_array($files)) {
683
            return true;
684
        }
685
686
        foreach ($files as $node) {
687
            if ($node['file'] === null) {
688
                continue;
689
            }
690
691
            $share_ref = [
692
                'id'    => $node['id'],
693
                'share' => $this->_id,
694
            ];
695
696
            $this->_db->{'fs.files'}->updateOne(
697
                [
698
                    '_id' => $node['file']
699
                ],
700
                [
701
                    '$addToSet' => [
702
                        'metadata.share_ref' => $share_ref,
703
                    ]
704
                ]
705
            );
706
        }
707
708
        return true;
709
    }
710
711
712
    /**
713
     * Unshare collection
714
     *
715
     * @return  bool
716
     */
717
    public function unshare(): bool
718
    {
719
        if (!$this->isAllowed('w')) {
720
            throw new Exception\Forbidden('not allowed to share node',
721
                Exception\Forbidden::NOT_ALLOWED_TO_SHARE
722
            );
723
        }
724
725
        if ($this->shared !== true) {
726
            throw new Exception\Conflict('Can not unshare a none shared collection',
727
                Exception\Conflict::NOT_SHARED
728
            );
729
        }
730
731
        $this->shared = false;
732
        $this->acl   = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $acl.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
733
        $action = [
734
            '$unset' => [
735
                'shared' => $this->_id
736
            ],
737
            '$set' => [
738
                'owner' => $this->_user->getId()
739
            ]
740
        ];
741
    
742
        $toset = $this->getChildrenRecursive($this->getRealId(), $shares, $files);
743
        
744
        
745
        $this->_db->storage->updateMany([
746
            '_id' => [
747
                '$in' => $toset,
748
            ],
749
        ], $action);
750
        
751
        $list = [];
0 ignored issues
show
Unused Code introduced by
$list 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...
752
        $result = $this->save(['shared'], ['acl']);
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...
753
754
        if (!is_array($files)) {
755
            return true;
756
        }
757
758
        foreach ($files as $node) {
759
            if ($node['file'] === null) {
760
                continue;
761
            }
762
763
            $share_ref = [
764
                'id'    => $node['id'],
765
                'share' => $this->_id,
766
            ];
767
768
            $this->_db->{'fs.files'}->updateOne(
769
                [
770
                    '_id' => $node['file']
771
                ],
772
                [
773
                '$pull' => [
774
                    'metadata.share_ref' => $share_ref,
775
                    ]
776
                ]
777
            );
778
        }
779
780
        return true;
781
    }
782
783
    
784
    /**
785
     * Get children
786
     *
787
     * @param   ObjectId $id
788
     * @param   array $shares
789
     * @param   arary $files
790
     * @return  array
791
     */
792
    public function getChildrenRecursive(?ObjectId $id=null, ?array &$shares=[], ?array &$files=[]): array
793
    {
794
        $list = [];
795
        $result = $this->_db->storage->find([
796
            'parent' => $id,
797
        ], [
798
            '_id' => 1,
799
            'directory' => 1,
800
            'reference' => 1,
801
            'shared'    => 1,
802
            'file'      => 1
803
        ]);
804
        
805
        foreach ($result as $node) {
806
            $list[] = $node['_id'];
807
808
            if ($node['directory'] === false) {
809
                $files[] = [
810
                    'id'    => $node['_id'],
811
                    'file'  => isset($node['file']) ? $node['file'] : null,
812
                ];
813
            } else {
814
                if (isset($node['reference']) || isset($node['shared']) && $node['shared'] === true) {
815
                    $shares[] = $node['_id'];
816
                }
817
        
818
                if ($node['directory'] === true && !isset($node['reference'])) {
819
                    $list = array_merge($list, $this->getChildrenRecursive($node['_id'], $shares, $files));
820
                }
821
            }
822
        }
823
        return $list;
824
    }
825
826
827
    /**
828
     * Create new directory
829
     *
830
     * @param   string $name
831
     * @param   arracy $attributes
832
     * @param   int $conflict
833
     * @param   bool $clone
834
     * @return  Collection
835
     */
836
    public function addDirectory($name, array $attributes=[], int $conflict=INode::CONFLICT_NOACTION, bool $clone=false): Collection
837
    {
838
        if (!$this->isAllowed('w')) {
839
            throw new Exception\Forbidden('not allowed to create new node here',
840
                Exception\Forbidden::NOT_ALLOWED_TO_CREATE
841
            );
842
        }
843
844
        $this->_pluginmgr->run('preCreateCollection', [$this, &$name, &$attributes, &$clone]);
845
846
        if ($this->readonly) {
847
            throw new Exception\Conflict('node is set as readonly, it is not possible to add new sub nodes',
848
                Exception\Conflict::READONLY
849
            );
850
        }
851
852
        $name = $this->checkName($name);
853
854
        if ($this->childExists($name)) {
855
            if ($conflict === INode::CONFLICT_NOACTION) {
856
                throw new Exception\Conflict('a node called '.$name.' does already exists in this collection',
857
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
858
                );
859
            } elseif ($conflict === INode::CONFLICT_RENAME) {
860
                $name = $this->getDuplicateName($name);
0 ignored issues
show
Bug introduced by
The method getDuplicateName() does not exist on Balloon\Filesystem\Node\Collection. Did you maybe mean _getDuplicateName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
861
            }
862
        }
863
        
864
        if ($this->isDeleted()) {
865
            throw new Exception\Conflict('could not add node '.$name.' into a deleted parent collection',
866
                Exception\Conflict::DELETED_PARENT
867
            );
868
        }
869
        
870
        try {
871
            $meta = [
872
                'name'      => $name,
873
                'deleted'   => false,
874
                'parent'    => $this->getRealId(),
875
                'directory' => true,
876
                'meta'      => [],
877
                'created'   => new UTCDateTime(),
878
                'changed'   => new UTCDateTime(),
879
                'owner'     => $this->_user->getId(),
880
                'shared'    => ($this->shared === true ? $this->getRealId() : $this->shared)
881
            ];
882
883
            $save  = array_merge($meta, $attributes);
884
885
            $result = $this->_db->storage->insertOne($save);
886
            $save['_id'] = $result->getInsertedId();
887
888
            $this->_logger->info('added new collection ['.$save['_id'].'] under parent ['.$this->_id.']', [
889
                'category' => get_class($this),
890
            ]);
891
892
            $new = new Collection(new BSONDocument($save), $this->_fs);
893
            $this->_pluginmgr->run('postCreateCollection', [$this, $new, $clone]);
894
     
895
            return $new;
896
        } catch (\Exception $e) {
897
            $this->_logger->error('failed create new collection under parent ['.$this->_id.']', [
898
                'category' => get_class($this),
899
                'exception'=> $e,
900
            ]);
901
902
            throw $e;
903
        }
904
    }
905
906
    /**
907
     * Create new file as a child from this collection
908
     *
909
     * @param   string $name
910
     * @param   ressource|string $data
911
     * @param   array $attributes
912
     * @param   int $conflict
913
     * @param   bool $clone
914
     * @return  File
915
     */
916
    public function addFile($name, $data=null, array $attributes=[], int $conflict=INode::CONFLICT_NOACTION, bool $clone=false): File
917
    {
918
        if (!$this->isAllowed('w')) {
919
            throw new Exception\Forbidden('not allowed to create new node here',
920
                Exception\Forbidden::NOT_ALLOWED_TO_CREATE
921
            );
922
        }
923
924
        $this->_pluginmgr->run('preCreateFile', [$this, &$name, &$attributes, &$clone]);
925
926
        if ($this->readonly) {
927
            throw new Exception\Conflict('node is set as readonly, it is not possible to add new sub nodes',
928
                Exception\Conflict::READONLY
929
            );
930
        }
931
932
        $name = $this->checkName($name);
933
934
        if ($this->childExists($name)) {
935
            if ($conflict === INode::CONFLICT_NOACTION) {
936
                throw new Exception\Conflict('a node called '.$name.' does already exists in this collection',
937
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
938
                );
939
            } elseif ($conflict === INode::CONFLICT_RENAME) {
940
                $name = $this->getDuplicateName($name);
0 ignored issues
show
Bug introduced by
The method getDuplicateName() does not exist on Balloon\Filesystem\Node\Collection. Did you maybe mean _getDuplicateName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
941
            }
942
        }
943
944
        if ($this->isDeleted()) {
945
            throw new Exception\Conflict('could not add node '.$name.' into a deleted parent collection',
946
                Exception\Conflict::DELETED_PARENT
947
            );
948
        }
949
950
        try {
951
            $meta = [
952
                'name'      => $name,
953
                'deleted'   => false,
954
                'parent'    => $this->getRealId(),
955
                'directory' => false,
956
                'hash'      => null,
957
                'meta'      => [],
958
                'created'   => new UTCDateTime(),
959
                'changed'   => new UTCDateTime(),
960
                'owner'     => $this->_user->getId(),
961
                'history'   => [],
962
                'version'   => 0,
963
                'shared'    => ($this->shared === true ? $this->getRealId() : $this->shared),
964
            ];
965
966
            $save  = array_merge($meta, $attributes);
967
968
            $result = $this->_db->storage->insertOne($save);
969
            $save['_id'] = $result->getInsertedId();
970
971
            $this->_logger->info('added new file ['.$save['_id'].'] under parent ['.$this->_id.']', [
972
                'category' => get_class($this),
973
            ]);
974
            
975
            $file = new File(new BSONDocument($save), $this->_fs);
976
977
            try {
978
                $file->put($data, true, $attributes);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 916 can also be of type null; however, Balloon\Filesystem\Node\File::put() does only seem to accept resource|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
979
            } catch (Exception\InsufficientStorage $e) {
980
                $this->_logger->warning('failed add new file under parent ['.$this->_id.'], cause user quota is full', [
981
                    'category' => get_class($this),
982
                    'exception' => $e,
983
                ]);
984
985
                $this->_db->storage->deleteOne([
986
                    '_id' => $save['_id']
987
                ]);
988
989
                throw $e;
990
            }
991
            
992
            $this->_pluginmgr->run('postCreateFile', [$this, $file, $clone]);
993
994
            return $file;
995
        } catch (\Exception $e) {
996
            $this->_logger->error('failed add new file under parent ['.$this->_id.']', [
997
                'category' => get_class($this),
998
                'exception' => $e,
999
            ]);
1000
1001
            throw $e;
1002
        }
1003
    }
1004
1005
1006
    /**
1007
     * Create new file wrapper
1008
     * (Sabe\DAV compatible method, elsewhere use addFile()
1009
     *
1010
     * Sabre\DAV requires that createFile() returns the ETag instead the newly created file instance
1011
     *
1012
     * @param   string $name
1013
     * @param   string $data
1014
     * @return  File
1015
     */
1016
    public function createFile($name, $data=null): String
1017
    {
1018
        return $this->addFile($name, $data)->getETag();
1019
    }
1020
    
1021
1022
    /**
1023
     * Create new directory wrapper
1024
     * (Sabe\DAV compatible method, elsewhere use addDirectory()
1025
     *
1026
     * Sabre\DAV requires that createDirectory() returns void
1027
     *
1028
     * @param   string $name
1029
     * @return  void
1030
     */
1031
    public function createDirectory($name): void
1032
    {
1033
        $this->addDirectory($name);
1034
    }
1035
}
1036