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