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\Hook; |
19
|
|
|
use Balloon\Server; |
20
|
|
|
use Balloon\Server\User; |
21
|
|
|
use MimeType\MimeType; |
22
|
|
|
use MongoDB\BSON\ObjectId; |
23
|
|
|
use MongoDB\BSON\UTCDateTime; |
24
|
|
|
use MongoDB\Database; |
25
|
|
|
use Normalizer; |
26
|
|
|
use Psr\Log\LoggerInterface; |
27
|
|
|
use ZipStream\ZipStream; |
28
|
|
|
|
29
|
|
|
abstract class AbstractNode implements NodeInterface |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* name max lenght. |
33
|
|
|
*/ |
34
|
|
|
const MAX_NAME_LENGTH = 255; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Unique id. |
38
|
|
|
* |
39
|
|
|
* @var ObjectId |
40
|
|
|
*/ |
41
|
|
|
protected $_id; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Node name. |
45
|
|
|
* |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
protected $name = ''; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Owner. |
52
|
|
|
* |
53
|
|
|
* @var ObjectId |
54
|
|
|
*/ |
55
|
|
|
protected $owner; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Mime. |
59
|
|
|
* |
60
|
|
|
* @var string |
61
|
|
|
*/ |
62
|
|
|
protected $mime; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Meta attributes. |
66
|
|
|
* |
67
|
|
|
* @var array |
68
|
|
|
*/ |
69
|
|
|
protected $meta = []; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* Parent collection. |
73
|
|
|
* |
74
|
|
|
* @var ObjectId |
75
|
|
|
*/ |
76
|
|
|
protected $parent; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Is file deleted. |
80
|
|
|
* |
81
|
|
|
* @var bool|UTCDateTime |
82
|
|
|
*/ |
83
|
|
|
protected $deleted = false; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Is shared? |
87
|
|
|
* |
88
|
|
|
* @var bool |
89
|
|
|
*/ |
90
|
|
|
protected $shared = false; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Destory at a certain time. |
94
|
|
|
* |
95
|
|
|
* @var UTCDateTime |
96
|
|
|
*/ |
97
|
|
|
protected $destroy; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Changed timestamp. |
101
|
|
|
* |
102
|
|
|
* @var UTCDateTime |
103
|
|
|
*/ |
104
|
|
|
protected $changed; |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Created timestamp. |
108
|
|
|
* |
109
|
|
|
* @var UTCDateTime |
110
|
|
|
*/ |
111
|
|
|
protected $created; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Point to antother node (Means this node is reference to $reference). |
115
|
|
|
* |
116
|
|
|
* @var ObjectId |
117
|
|
|
*/ |
118
|
|
|
protected $reference; |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Raw attributes before any processing or modifications. |
122
|
|
|
* |
123
|
|
|
* @var array |
124
|
|
|
*/ |
125
|
|
|
protected $raw_attributes; |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Readonly flag. |
129
|
|
|
* |
130
|
|
|
* @var bool |
131
|
|
|
*/ |
132
|
|
|
protected $readonly = false; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* App attributes. |
136
|
|
|
* |
137
|
|
|
* @var array |
138
|
|
|
*/ |
139
|
|
|
protected $app = []; |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Filesystem. |
143
|
|
|
* |
144
|
|
|
* @var Filesystem |
145
|
|
|
*/ |
146
|
|
|
protected $_fs; |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Database. |
150
|
|
|
* |
151
|
|
|
* @var Database |
152
|
|
|
*/ |
153
|
|
|
protected $_db; |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* User. |
157
|
|
|
* |
158
|
|
|
* @var User |
159
|
|
|
*/ |
160
|
|
|
protected $_user; |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Logger. |
164
|
|
|
* |
165
|
|
|
* @var LoggerInterface |
166
|
|
|
*/ |
167
|
|
|
protected $_logger; |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Server. |
171
|
|
|
* |
172
|
|
|
* @var Server |
173
|
|
|
*/ |
174
|
|
|
protected $_server; |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Hook. |
178
|
|
|
* |
179
|
|
|
* @var Hook |
180
|
|
|
*/ |
181
|
|
|
protected $_hook; |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Acl. |
185
|
|
|
* |
186
|
|
|
* @var Acl |
187
|
|
|
*/ |
188
|
|
|
protected $_acl; |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Mount. |
192
|
|
|
* |
193
|
|
|
* @var ObjectId |
194
|
|
|
*/ |
195
|
|
|
protected $storage_reference; |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Storage attributes. |
199
|
|
|
* |
200
|
|
|
* @var array |
201
|
|
|
*/ |
202
|
|
|
protected $storage; |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* File size for files, number of children for directories. |
206
|
|
|
* |
207
|
|
|
* @var int |
208
|
|
|
*/ |
209
|
|
|
protected $size = 0; |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Acl. |
213
|
|
|
* |
214
|
|
|
* @var array |
215
|
|
|
*/ |
216
|
|
|
protected $acl = []; |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Mount. |
220
|
|
|
* |
221
|
|
|
* @var array |
222
|
|
|
*/ |
223
|
|
|
protected $mount = []; |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Lock. |
227
|
|
|
* |
228
|
|
|
* @var array |
229
|
|
|
*/ |
230
|
|
|
protected $lock; |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Parent collection. |
234
|
|
|
* |
235
|
|
|
* @var Collection |
236
|
|
|
*/ |
237
|
|
|
protected $_parent; |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Convert to filename. |
241
|
|
|
* |
242
|
|
|
* @return string |
243
|
|
|
*/ |
244
|
|
|
public function __toString() |
245
|
|
|
{ |
246
|
|
|
return $this->name; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Get owner. |
251
|
|
|
*/ |
252
|
|
|
public function getOwner(): ObjectId |
253
|
|
|
{ |
254
|
|
|
return $this->owner; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Set filesystem. |
259
|
|
|
*/ |
260
|
|
|
public function setFilesystem(Filesystem $fs): NodeInterface |
261
|
|
|
{ |
262
|
|
|
$this->_fs = $fs; |
263
|
|
|
$this->_user = $fs->getUser(); |
264
|
|
|
|
265
|
|
|
return $this; |
|
|
|
|
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Get filesystem. |
270
|
|
|
*/ |
271
|
|
|
public function getFilesystem(): Filesystem |
272
|
|
|
{ |
273
|
|
|
return $this->_fs; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Check if $node is a sub node of any parent nodes of this node. |
278
|
|
|
*/ |
279
|
|
|
public function isSubNode(NodeInterface $node): bool |
280
|
|
|
{ |
281
|
|
|
if ($node->getId() == $this->_id) { |
282
|
|
|
return true; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
foreach ($node->getParents() as $node) { |
286
|
|
|
if ($node->getId() == $this->_id) { |
287
|
|
|
return true; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
if ($this->isRoot()) { |
292
|
|
|
return true; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
return false; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Move node. |
300
|
|
|
*/ |
301
|
|
|
public function setParent(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION): NodeInterface |
302
|
|
|
{ |
303
|
|
|
if ($this->parent == $parent->getId()) { |
304
|
|
|
throw new Exception\Conflict( |
305
|
|
|
'source node '.$this->name.' is already in the requested parent folder', |
306
|
|
|
Exception\Conflict::ALREADY_THERE |
307
|
|
|
); |
308
|
|
|
} |
309
|
|
|
if ($this->isSubNode($parent)) { |
310
|
|
|
throw new Exception\Conflict( |
311
|
|
|
'node called '.$this->name.' can not be moved into itself', |
312
|
|
|
Exception\Conflict::CANT_BE_CHILD_OF_ITSELF |
313
|
|
|
); |
314
|
|
|
} |
315
|
|
|
if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) { |
316
|
|
|
throw new ForbiddenException( |
317
|
|
|
'not allowed to move node '.$this->name, |
318
|
|
|
ForbiddenException::NOT_ALLOWED_TO_MOVE |
319
|
|
|
); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
$new_name = $parent->validateInsert($this->name, $conflict, get_class($this)); |
323
|
|
|
|
324
|
|
|
if ($this->isShared() && $this instanceof Collection && $parent->isShared()) { |
325
|
|
|
throw new Exception\Conflict( |
326
|
|
|
'a shared folder can not be a child of a shared folder', |
327
|
|
|
Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE |
328
|
|
|
); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
if (NodeInterface::CONFLICT_RENAME === $conflict && $new_name !== $this->name) { |
332
|
|
|
$this->setName($new_name); |
333
|
|
|
$this->raw_attributes['name'] = $this->name; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
if ($this instanceof Collection) { |
337
|
|
|
$query = [ |
338
|
|
|
'$or' => [ |
339
|
|
|
['reference' => ['exists' => true]], |
340
|
|
|
['shared' => true], |
341
|
|
|
], |
342
|
|
|
]; |
343
|
|
|
|
344
|
|
|
if ($parent->isShared() && iterator_count($this->_fs->findNodesByFilterRecursive($this, $query, 0, 1)) !== 0) { |
|
|
|
|
345
|
|
|
throw new Exception\Conflict( |
346
|
|
|
'folder contains a shared folder', |
347
|
|
|
Exception\Conflict::NODE_CONTAINS_SHARED_NODE |
348
|
|
|
); |
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
if ($this->isShared() && $parent->isSpecial()) { |
353
|
|
|
throw new Exception\Conflict( |
354
|
|
|
'a shared folder can not be an indirect child of a shared folder', |
355
|
|
|
Exception\Conflict::SHARED_NODE_CANT_BE_INDIRECT_CHILD_OF_SHARE |
356
|
|
|
); |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
if (($parent->isSpecial() && $this->shared != $parent->getShareId()) |
360
|
|
|
|| (!$parent->isSpecial() && $this->isShareMember()) |
361
|
|
|
|| ($parent->getMount() != $this->getParent()->getMount())) { |
362
|
|
|
$new = $this->copyTo($parent, $conflict); |
363
|
|
|
$this->delete(); |
364
|
|
|
|
365
|
|
|
return $new; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
if ($parent->childExists($this->name) && NodeInterface::CONFLICT_MERGE === $conflict) { |
369
|
|
|
$new = $this->copyTo($parent, $conflict); |
370
|
|
|
$this->delete(true); |
371
|
|
|
|
372
|
|
|
return $new; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
$this->storage = $this->_parent->getStorage()->move($this, $parent); |
376
|
|
|
$this->parent = $parent->getRealId(); |
377
|
|
|
$this->owner = $this->_user->getId(); |
378
|
|
|
|
379
|
|
|
$this->save(['parent', 'shared', 'owner', 'storage']); |
380
|
|
|
|
381
|
|
|
return $this; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Lock file. |
386
|
|
|
*/ |
387
|
|
|
public function lock(string $identifier, ?int $ttl = 1800): NodeInterface |
388
|
|
|
{ |
389
|
|
|
if ($this->isLocked()) { |
390
|
|
|
if ($identifier !== $this->lock['id']) { |
391
|
|
|
throw new Exception\LockIdMissmatch('the unlock id must match the current lock id'); |
392
|
|
|
} |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
$this->lock = $this->prepareLock($identifier, $ttl ?? 1800); |
396
|
|
|
$this->save(['lock']); |
397
|
|
|
|
398
|
|
|
return $this; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Get lock. |
403
|
|
|
*/ |
404
|
|
|
public function getLock(): array |
405
|
|
|
{ |
406
|
|
|
if (!$this->isLocked()) { |
407
|
|
|
throw new Exception\NotLocked('node is not locked'); |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
return $this->lock; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
/** |
414
|
|
|
* Is locked? |
415
|
|
|
*/ |
416
|
|
|
public function isLocked(): bool |
417
|
|
|
{ |
418
|
|
|
if ($this->lock === null) { |
419
|
|
|
return false; |
420
|
|
|
} |
421
|
|
|
if ($this->lock['expire'] <= new UTCDateTime()) { |
422
|
|
|
return false; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
return true; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* Unlock. |
430
|
|
|
*/ |
431
|
|
|
public function unlock(?string $identifier = null): NodeInterface |
432
|
|
|
{ |
433
|
|
|
if (!$this->isLocked()) { |
434
|
|
|
throw new Exception\NotLocked('node is not locked'); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
if ($this->lock['owner'] != $this->_user->getId()) { |
438
|
|
|
throw new Exception\Forbidden('node is locked by another user'); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
if ($identifier !== null && $this->lock['id'] !== $identifier) { |
442
|
|
|
throw new Exception\LockIdMissmatch('the unlock id must match the current lock id'); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
$this->lock = null; |
|
|
|
|
446
|
|
|
$this->save(['lock']); |
447
|
|
|
|
448
|
|
|
return $this; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
/** |
452
|
|
|
* Set node acl. |
453
|
|
|
*/ |
454
|
|
|
public function setAcl(array $acl): NodeInterface |
455
|
|
|
{ |
456
|
|
|
if (!$this->_acl->isAllowed($this, 'm')) { |
457
|
|
|
throw new ForbiddenException( |
458
|
|
|
'not allowed to update acl', |
459
|
|
|
ForbiddenException::NOT_ALLOWED_TO_MANAGE |
460
|
|
|
); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
if (!$this->isShareMember()) { |
464
|
|
|
throw new Exception\Conflict('node acl may only be set on share member nodes', Exception\Conflict::NOT_SHARED); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
$this->_acl->validateAcl($this->_server, $acl); |
468
|
|
|
$this->acl = $acl; |
469
|
|
|
$this->save(['acl']); |
470
|
|
|
|
471
|
|
|
return $this; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Get ACL. |
476
|
|
|
*/ |
477
|
|
|
public function getAcl(): array |
478
|
|
|
{ |
479
|
|
|
if ($this->isReference()) { |
480
|
|
|
$acl = $this->_fs->findRawNode($this->getShareId())['acl']; |
|
|
|
|
481
|
|
|
} else { |
482
|
|
|
$acl = $this->acl; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
return $this->_acl->resolveAclTable($this->_server, $acl); |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* Get share id. |
490
|
|
|
*/ |
491
|
|
|
public function getShareId(bool $reference = false): ?ObjectId |
492
|
|
|
{ |
493
|
|
|
if ($this->isReference() && true === $reference) { |
494
|
|
|
return $this->_id; |
495
|
|
|
} |
496
|
|
|
if ($this->isShareMember() && true === $reference) { |
497
|
|
|
return $this->shared; |
498
|
|
|
} |
499
|
|
|
if ($this->isShared() && $this->isReference()) { |
500
|
|
|
return $this->reference; |
501
|
|
|
} |
502
|
|
|
if ($this->isShared()) { |
503
|
|
|
return $this->_id; |
504
|
|
|
} |
505
|
|
|
if ($this->isShareMember()) { |
506
|
|
|
return $this->shared; |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
return null; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* Get reference. |
514
|
|
|
*/ |
515
|
|
|
public function getReference(): ?ObjectId |
516
|
|
|
{ |
517
|
|
|
return $this->reference; |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* Get share node. |
522
|
|
|
*/ |
523
|
|
|
public function getShareNode(): ?Collection |
524
|
|
|
{ |
525
|
|
|
if ($this->isShare()) { |
526
|
|
|
return $this; |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
if ($this->isSpecial()) { |
530
|
|
|
return $this->_fs->findNodeById($this->getShareId(true)); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
return null; |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
/** |
537
|
|
|
* Is node marked as readonly? |
538
|
|
|
*/ |
539
|
|
|
public function isReadonly(): bool |
540
|
|
|
{ |
541
|
|
|
return $this->readonly; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
/** |
545
|
|
|
* May write. |
546
|
|
|
*/ |
547
|
|
|
public function mayWrite(): bool |
548
|
|
|
{ |
549
|
|
|
return Acl::PRIVILEGES_WEIGHT[$this->_acl->getAclPrivilege($this)] > Acl::PRIVILEGE_READ; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* Request is from node owner? |
554
|
|
|
*/ |
555
|
|
|
public function isOwnerRequest(): bool |
556
|
|
|
{ |
557
|
|
|
return null !== $this->_user && $this->owner == $this->_user->getId(); |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
/** |
561
|
|
|
* Check if node is kind of special. |
562
|
|
|
*/ |
563
|
|
|
public function isSpecial(): bool |
564
|
|
|
{ |
565
|
|
|
if ($this->isShared()) { |
566
|
|
|
return true; |
567
|
|
|
} |
568
|
|
|
if ($this->isReference()) { |
569
|
|
|
return true; |
570
|
|
|
} |
571
|
|
|
if ($this->isShareMember()) { |
572
|
|
|
return true; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
return false; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Check if node is a sub node of a share. |
580
|
|
|
*/ |
581
|
|
|
public function isShareMember(): bool |
582
|
|
|
{ |
583
|
|
|
return $this->shared instanceof ObjectId && !$this->isReference(); |
|
|
|
|
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
/** |
587
|
|
|
* Check if node is a sub node of an external storage mount. |
588
|
|
|
*/ |
589
|
|
|
public function isMountMember(): bool |
590
|
|
|
{ |
591
|
|
|
return $this->storage_reference instanceof ObjectId; |
|
|
|
|
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
/** |
595
|
|
|
* Is share. |
596
|
|
|
*/ |
597
|
|
|
public function isShare(): bool |
598
|
|
|
{ |
599
|
|
|
return true === $this->shared && !$this->isReference(); |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
/** |
603
|
|
|
* Is share (Reference or master share). |
604
|
|
|
*/ |
605
|
|
|
public function isShared(): bool |
606
|
|
|
{ |
607
|
|
|
if (true === $this->shared) { |
608
|
|
|
return true; |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
return false; |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
/** |
615
|
|
|
* Set the name. |
616
|
|
|
*/ |
617
|
|
|
public function setName($name): bool |
618
|
|
|
{ |
619
|
|
|
$name = $this->checkName($name); |
620
|
|
|
|
621
|
|
|
try { |
622
|
|
|
$child = $this->getParent()->getChild($name); |
623
|
|
|
if ($child->getId() != $this->_id) { |
624
|
|
|
throw new Exception\Conflict( |
625
|
|
|
'a node called '.$name.' does already exists in this collection', |
626
|
|
|
Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS |
627
|
|
|
); |
628
|
|
|
} |
629
|
|
|
} catch (Exception\NotFound $e) { |
630
|
|
|
//child does not exists, we can safely rename |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
$this->storage = $this->_parent->getStorage()->rename($this, $name); |
634
|
|
|
$this->name = $name; |
635
|
|
|
|
636
|
|
|
if ($this instanceof File) { |
637
|
|
|
$this->mime = MimeType::getType($this->name); |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
return $this->save(['name', 'storage', 'mime']); |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
/** |
644
|
|
|
* Check name. |
645
|
|
|
*/ |
646
|
|
|
public function checkName(string $name): string |
647
|
|
|
{ |
648
|
|
|
if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) { |
649
|
|
|
throw new Exception\InvalidArgument('name contains invalid characters'); |
650
|
|
|
} |
651
|
|
|
if (strlen($name) > self::MAX_NAME_LENGTH) { |
652
|
|
|
throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters'); |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
if (!Normalizer::isNormalized($name)) { |
656
|
|
|
$name = Normalizer::normalize($name); |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
return $name; |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
/** |
663
|
|
|
* Get the name. |
664
|
|
|
*/ |
665
|
|
|
public function getName(): string |
666
|
|
|
{ |
667
|
|
|
return $this->name; |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
/** |
671
|
|
|
* Get mount node. |
672
|
|
|
*/ |
673
|
|
|
public function getMount(): ?ObjectId |
674
|
|
|
{ |
675
|
|
|
return count($this->mount) > 0 ? $this->_id : $this->storage_reference; |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
/** |
679
|
|
|
* Undelete. |
680
|
|
|
*/ |
681
|
|
|
public function undelete(int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): bool |
682
|
|
|
{ |
683
|
|
|
if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) { |
684
|
|
|
throw new ForbiddenException( |
685
|
|
|
'not allowed to restore node '.$this->name, |
686
|
|
|
ForbiddenException::NOT_ALLOWED_TO_UNDELETE |
687
|
|
|
); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
$parent = $this->getParent(); |
691
|
|
|
if ($parent->isDeleted()) { |
692
|
|
|
throw new Exception\Conflict( |
693
|
|
|
'could not restore node '.$this->name.' into a deleted parent', |
694
|
|
|
Exception\Conflict::DELETED_PARENT |
695
|
|
|
); |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
if ($parent->childExists($this->name)) { |
699
|
|
|
if (NodeInterface::CONFLICT_MERGE === $conflict) { |
700
|
|
|
$new = $this->copyTo($parent, $conflict, null, true, NodeInterface::DELETED_INCLUDE); |
701
|
|
|
|
702
|
|
|
if ($new->getId() != $this->getId()) { |
703
|
|
|
$this->delete(true); |
704
|
|
|
} |
705
|
|
|
} elseif (NodeInterface::CONFLICT_RENAME === $conflict) { |
706
|
|
|
$this->setName($this->getDuplicateName()); |
707
|
|
|
$this->raw_attributes['name'] = $this->name; |
708
|
|
|
} else { |
709
|
|
|
throw new Exception\Conflict( |
710
|
|
|
'a node called '.$this->name.' does already exists in this collection', |
711
|
|
|
Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS |
712
|
|
|
); |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
if (null === $recursion) { |
717
|
|
|
$recursion_first = true; |
718
|
|
|
$recursion = uniqid(); |
719
|
|
|
} else { |
720
|
|
|
$recursion_first = false; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
$this->storage = $this->_parent->getStorage()->undelete($this); |
724
|
|
|
$this->deleted = false; |
725
|
|
|
|
726
|
|
|
$this->save([ |
727
|
|
|
'storage', |
728
|
|
|
'name', |
729
|
|
|
'deleted', |
730
|
|
|
], [], $recursion, $recursion_first); |
731
|
|
|
|
732
|
|
|
if ($this instanceof File || $this->isReference() || $this->isMounted() || $this->isFiltered()) { |
|
|
|
|
733
|
|
|
return true; |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
return $this->doRecursiveAction(function ($node) use ($conflict, $recursion) { |
|
|
|
|
737
|
|
|
$node->undelete($conflict, $recursion, false); |
738
|
|
|
}, NodeInterface::DELETED_ONLY); |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Is node deleted? |
743
|
|
|
*/ |
744
|
|
|
public function isDeleted(): bool |
745
|
|
|
{ |
746
|
|
|
return $this->deleted instanceof UTCDateTime; |
|
|
|
|
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
/** |
750
|
|
|
* Get last modified timestamp. |
751
|
|
|
*/ |
752
|
|
|
public function getLastModified(): int |
753
|
|
|
{ |
754
|
|
|
if ($this->changed instanceof UTCDateTime) { |
|
|
|
|
755
|
|
|
return (int) $this->changed->toDateTime()->format('U'); |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
return 0; |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* Get unique id. |
763
|
|
|
*/ |
764
|
1 |
|
public function getId(): ?ObjectId |
765
|
|
|
{ |
766
|
1 |
|
return $this->_id; |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Get parent. |
771
|
|
|
*/ |
772
|
|
|
public function getParent(): ?Collection |
773
|
|
|
{ |
774
|
|
|
return $this->_parent; |
775
|
|
|
} |
776
|
|
|
|
777
|
|
|
/** |
778
|
|
|
* Get parents. |
779
|
|
|
*/ |
780
|
|
|
public function getParents(?NodeInterface $node = null, array $parents = []): array |
781
|
|
|
{ |
782
|
|
|
if (null === $node) { |
783
|
|
|
$node = $this; |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
if ($node->isInRoot()) { |
787
|
|
|
return $parents; |
788
|
|
|
} |
789
|
|
|
$parent = $node->getParent(); |
790
|
|
|
$parents[] = $parent; |
791
|
|
|
|
792
|
|
|
return $node->getParents($parent, $parents); |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
/** |
796
|
|
|
* Get as zip. |
797
|
|
|
*/ |
798
|
|
|
public function getZip(): void |
799
|
|
|
{ |
800
|
|
|
set_time_limit(0); |
801
|
|
|
$archive = new ZipStream($this->name.'.zip'); |
802
|
|
|
$this->zip($archive, false); |
803
|
|
|
$archive->finish(); |
804
|
|
|
} |
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* Create zip. |
808
|
|
|
*/ |
809
|
|
|
public function zip(ZipStream $archive, bool $self = true, ?NodeInterface $parent = null, string $path = '', int $depth = 0): bool |
810
|
|
|
{ |
811
|
|
|
if (null === $parent) { |
812
|
|
|
$parent = $this; |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
if ($parent instanceof Collection) { |
816
|
|
|
$children = $parent->getChildNodes(); |
817
|
|
|
|
818
|
|
|
if (true === $self && 0 === $depth) { |
819
|
|
|
$path = $parent->getName().DIRECTORY_SEPARATOR; |
820
|
|
|
} elseif (0 === $depth) { |
821
|
|
|
$path = ''; |
822
|
|
|
} elseif (0 !== $depth) { |
823
|
|
|
$path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR; |
824
|
|
|
} |
825
|
|
|
|
826
|
|
|
foreach ($children as $child) { |
827
|
|
|
$name = $path.$child->getName(); |
828
|
|
|
|
829
|
|
|
if ($child instanceof Collection) { |
830
|
|
|
$this->zip($archive, $self, $child, $name, ++$depth); |
831
|
|
|
} elseif ($child instanceof File) { |
832
|
|
|
try { |
833
|
|
|
$resource = $child->get(); |
834
|
|
|
if ($resource !== null) { |
835
|
|
|
$archive->addFileFromStream($name, $resource); |
836
|
|
|
} |
837
|
|
|
} catch (\Exception $e) { |
838
|
|
|
$this->_logger->error('failed add file ['.$child->getId().'] to zip stream', [ |
839
|
|
|
'category' => get_class($this), |
840
|
|
|
'exception' => $e, |
841
|
|
|
]); |
842
|
|
|
} |
843
|
|
|
} |
844
|
|
|
} |
845
|
|
|
} elseif ($parent instanceof File) { |
846
|
|
|
$resource = $parent->get(); |
847
|
|
|
if ($resource !== null) { |
848
|
|
|
$archive->addFileFromStream($parent->getName(), $resource); |
849
|
|
|
} |
850
|
|
|
} |
851
|
|
|
|
852
|
|
|
return true; |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
/** |
856
|
|
|
* Get mime type. |
857
|
|
|
*/ |
858
|
1 |
|
public function getContentType(): string |
859
|
|
|
{ |
860
|
1 |
|
return $this->mime; |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
/** |
864
|
|
|
* Is reference. |
865
|
|
|
*/ |
866
|
|
|
public function isReference(): bool |
867
|
|
|
{ |
868
|
|
|
return $this->reference instanceof ObjectId; |
|
|
|
|
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
/** |
872
|
|
|
* Set app attributes. |
873
|
|
|
*/ |
874
|
|
|
public function setAppAttributes(string $namespace, array $attributes): NodeInterface |
875
|
|
|
{ |
876
|
|
|
$this->app[$namespace] = $attributes; |
877
|
|
|
$this->save('app.'.$namespace); |
878
|
|
|
|
879
|
|
|
return $this; |
880
|
|
|
} |
881
|
|
|
|
882
|
|
|
/** |
883
|
|
|
* Set app attribute. |
884
|
|
|
*/ |
885
|
|
|
public function setAppAttribute(string $namespace, string $attribute, $value): NodeInterface |
886
|
|
|
{ |
887
|
|
|
if (!isset($this->app[$namespace])) { |
888
|
|
|
$this->app[$namespace] = []; |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
$this->app[$namespace][$attribute] = $value; |
892
|
|
|
$this->save('app.'.$namespace); |
893
|
|
|
|
894
|
|
|
return $this; |
895
|
|
|
} |
896
|
|
|
|
897
|
|
|
/** |
898
|
|
|
* Remove app attribute. |
899
|
|
|
*/ |
900
|
|
|
public function unsetAppAttributes(string $namespace): NodeInterface |
901
|
|
|
{ |
902
|
|
|
if (isset($this->app[$namespace])) { |
903
|
|
|
unset($this->app[$namespace]); |
904
|
|
|
$this->save('app.'.$namespace); |
905
|
|
|
} |
906
|
|
|
|
907
|
|
|
return $this; |
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
/** |
911
|
|
|
* Remove app attribute. |
912
|
|
|
*/ |
913
|
|
|
public function unsetAppAttribute(string $namespace, string $attribute): NodeInterface |
914
|
|
|
{ |
915
|
|
|
if (isset($this->app[$namespace][$attribute])) { |
916
|
|
|
unset($this->app[$namespace][$attribute]); |
917
|
|
|
$this->save('app'.$namespace); |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
return $this; |
921
|
|
|
} |
922
|
|
|
|
923
|
|
|
/** |
924
|
|
|
* Get app attribute. |
925
|
|
|
*/ |
926
|
|
|
public function getAppAttribute(string $namespace, string $attribute) |
927
|
|
|
{ |
928
|
|
|
if (isset($this->app[$namespace][$attribute])) { |
929
|
|
|
return $this->app[$namespace][$attribute]; |
930
|
|
|
} |
931
|
|
|
|
932
|
|
|
return null; |
933
|
|
|
} |
934
|
|
|
|
935
|
|
|
/** |
936
|
|
|
* Get app attributes. |
937
|
|
|
*/ |
938
|
|
|
public function getAppAttributes(string $namespace): array |
939
|
|
|
{ |
940
|
|
|
if (isset($this->app[$namespace])) { |
941
|
|
|
return $this->app[$namespace]; |
942
|
|
|
} |
943
|
|
|
|
944
|
|
|
return []; |
945
|
|
|
} |
946
|
|
|
|
947
|
|
|
/** |
948
|
|
|
* Set meta attributes. |
949
|
|
|
*/ |
950
|
|
|
public function setMetaAttributes(array $attributes): NodeInterface |
951
|
|
|
{ |
952
|
|
|
$attributes = $this->validateMetaAttributes($attributes); |
953
|
|
|
foreach ($attributes as $attribute => $value) { |
954
|
|
|
if (empty($value) && isset($this->meta[$attribute])) { |
955
|
|
|
unset($this->meta[$attribute]); |
956
|
|
|
} elseif (!empty($value)) { |
957
|
|
|
$this->meta[$attribute] = $value; |
958
|
|
|
} |
959
|
|
|
} |
960
|
|
|
|
961
|
|
|
$this->save('meta'); |
962
|
|
|
|
963
|
|
|
return $this; |
964
|
|
|
} |
965
|
|
|
|
966
|
|
|
/** |
967
|
|
|
* Get meta attributes as array. |
968
|
|
|
*/ |
969
|
|
|
public function getMetaAttributes(array $attributes = []): array |
970
|
|
|
{ |
971
|
|
|
if (empty($attributes)) { |
972
|
|
|
return $this->meta; |
973
|
|
|
} |
974
|
|
|
if (is_array($attributes)) { |
975
|
|
|
return array_intersect_key($this->meta, array_flip($attributes)); |
976
|
|
|
} |
977
|
|
|
} |
978
|
|
|
|
979
|
|
|
/** |
980
|
|
|
* Mark node as readonly. |
981
|
|
|
*/ |
982
|
|
|
public function setReadonly(bool $readonly = true): bool |
983
|
|
|
{ |
984
|
|
|
$this->readonly = $readonly; |
985
|
|
|
$this->storage = $this->_parent->getStorage()->readonly($this, $readonly); |
986
|
|
|
|
987
|
|
|
return $this->save(['readonly', 'storage']); |
988
|
|
|
} |
989
|
|
|
|
990
|
|
|
/** |
991
|
|
|
* Mark node as self-destroyable. |
992
|
|
|
*/ |
993
|
|
|
public function setDestroyable(?UTCDateTime $ts): bool |
994
|
|
|
{ |
995
|
|
|
$this->destroy = $ts; |
996
|
|
|
|
997
|
|
|
if (null === $ts) { |
998
|
|
|
return $this->save([], 'destroy'); |
999
|
|
|
} |
1000
|
|
|
|
1001
|
|
|
return $this->save('destroy'); |
1002
|
|
|
} |
1003
|
|
|
|
1004
|
|
|
/** |
1005
|
|
|
* Get original raw attributes before any processing. |
1006
|
|
|
*/ |
1007
|
|
|
public function getRawAttributes(): array |
1008
|
|
|
{ |
1009
|
|
|
return $this->raw_attributes; |
1010
|
|
|
} |
1011
|
|
|
|
1012
|
|
|
/** |
1013
|
|
|
* Check if node is in root. |
1014
|
|
|
*/ |
1015
|
|
|
public function isInRoot(): bool |
1016
|
|
|
{ |
1017
|
|
|
return null === $this->parent; |
1018
|
|
|
} |
1019
|
|
|
|
1020
|
|
|
/** |
1021
|
|
|
* Check if node is an instance of the actual root collection. |
1022
|
|
|
*/ |
1023
|
|
|
public function isRoot(): bool |
1024
|
|
|
{ |
1025
|
|
|
return null === $this->_id && ($this instanceof Collection); |
1026
|
|
|
} |
1027
|
|
|
|
1028
|
|
|
/** |
1029
|
|
|
* Resolve node path. |
1030
|
|
|
*/ |
1031
|
|
|
public function getPath(): string |
1032
|
|
|
{ |
1033
|
|
|
$path = ''; |
1034
|
|
|
foreach (array_reverse($this->getParents()) as $parent) { |
1035
|
|
|
$path .= DIRECTORY_SEPARATOR.$parent->getName(); |
1036
|
|
|
} |
1037
|
|
|
|
1038
|
|
|
$path .= DIRECTORY_SEPARATOR.$this->getName(); |
1039
|
|
|
|
1040
|
|
|
return $path; |
1041
|
|
|
} |
1042
|
|
|
|
1043
|
|
|
/** |
1044
|
|
|
* Save node attributes. |
1045
|
|
|
* |
1046
|
|
|
* @param array|string $attributes |
1047
|
|
|
* @param array|string $remove |
1048
|
|
|
* @param string $recursion |
1049
|
|
|
*/ |
1050
|
|
|
public function save($attributes = [], $remove = [], ?string $recursion = null, bool $recursion_first = true): bool |
1051
|
|
|
{ |
1052
|
|
|
if (!$this->_acl->isAllowed($this, 'w') && !$this->isReference()) { |
1053
|
|
|
throw new ForbiddenException( |
1054
|
|
|
'not allowed to modify node '.$this->name, |
1055
|
|
|
ForbiddenException::NOT_ALLOWED_TO_MODIFY |
1056
|
|
|
); |
1057
|
|
|
} |
1058
|
|
|
|
1059
|
|
|
if ($this instanceof Collection && $this->isRoot()) { |
1060
|
|
|
return false; |
1061
|
|
|
} |
1062
|
|
|
|
1063
|
|
|
$remove = (array) $remove; |
1064
|
|
|
$attributes = (array) $attributes; |
1065
|
|
|
$this->_hook->run( |
1066
|
|
|
'preSaveNodeAttributes', |
1067
|
|
|
[$this, &$attributes, &$remove, &$recursion, &$recursion_first] |
1068
|
|
|
); |
1069
|
|
|
|
1070
|
|
|
try { |
1071
|
|
|
$set = []; |
1072
|
|
|
$values = $this->getAttributes(); |
1073
|
|
|
foreach ($attributes as $attr) { |
1074
|
|
|
$set[$attr] = $this->getArrayValue($values, $attr); |
1075
|
|
|
} |
1076
|
|
|
|
1077
|
|
|
$update = []; |
1078
|
|
|
if (!empty($set)) { |
1079
|
|
|
$update['$set'] = $set; |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
if (!empty($remove)) { |
1083
|
|
|
$remove = array_fill_keys($remove, 1); |
1084
|
|
|
$update['$unset'] = $remove; |
1085
|
|
|
} |
1086
|
|
|
|
1087
|
|
|
if (empty($update)) { |
1088
|
|
|
return false; |
1089
|
|
|
} |
1090
|
|
|
$result = $this->_db->storage->updateOne([ |
|
|
|
|
1091
|
|
|
'_id' => $this->_id, |
1092
|
|
|
], $update); |
1093
|
|
|
|
1094
|
|
|
$this->_hook->run( |
1095
|
|
|
'postSaveNodeAttributes', |
1096
|
|
|
[$this, $attributes, $remove, $recursion, $recursion_first] |
1097
|
|
|
); |
1098
|
|
|
|
1099
|
|
|
$this->_logger->info('modified node attributes of ['.$this->_id.']', [ |
1100
|
|
|
'category' => get_class($this), |
1101
|
|
|
'params' => $update, |
1102
|
|
|
]); |
1103
|
|
|
|
1104
|
|
|
return true; |
1105
|
|
|
} catch (\Exception $e) { |
1106
|
|
|
$this->_logger->error('failed modify node attributes of ['.$this->_id.']', [ |
1107
|
|
|
'category' => get_class($this), |
1108
|
|
|
'exception' => $e, |
1109
|
|
|
]); |
1110
|
|
|
|
1111
|
|
|
throw $e; |
1112
|
|
|
} |
1113
|
|
|
} |
1114
|
|
|
|
1115
|
|
|
/** |
1116
|
|
|
* Duplicate name with a uniqid within name. |
1117
|
|
|
*/ |
1118
|
|
|
public function getDuplicateName(?string $name = null, ?string $class = null): string |
1119
|
|
|
{ |
1120
|
|
|
if (null === $name) { |
1121
|
|
|
$name = $this->name; |
1122
|
|
|
} |
1123
|
|
|
|
1124
|
|
|
if (null === $class) { |
1125
|
|
|
$class = get_class($this); |
1126
|
|
|
} |
1127
|
|
|
|
1128
|
|
|
if ($class === Collection::class) { |
1129
|
|
|
return $name.' ('.substr(uniqid('', true), -4).')'; |
1130
|
|
|
} |
1131
|
|
|
|
1132
|
|
|
$ext = substr(strrchr($name, '.'), 1); |
1133
|
|
|
if (false === $ext) { |
1134
|
|
|
return $name.' ('.substr(uniqid('', true), -4).')'; |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
|
|
$name = substr($name, 0, -(strlen($ext) + 1)); |
1138
|
|
|
|
1139
|
|
|
return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext; |
1140
|
|
|
} |
1141
|
|
|
|
1142
|
|
|
/** |
1143
|
|
|
* Prepare lock. |
1144
|
|
|
*/ |
1145
|
|
|
protected function prepareLock(string $identifier, int $ttl = 1800): array |
1146
|
|
|
{ |
1147
|
|
|
return [ |
1148
|
|
|
'owner' => $this->_user->getId(), |
1149
|
|
|
'created' => new UTCDateTime(), |
1150
|
|
|
'id' => $identifier, |
1151
|
|
|
'expire' => new UTCDateTime((time() + $ttl) * 1000), |
1152
|
|
|
]; |
1153
|
|
|
} |
1154
|
|
|
|
1155
|
|
|
/** |
1156
|
|
|
* Get array value via string path. |
1157
|
|
|
*/ |
1158
|
|
|
protected function getArrayValue(iterable $array, string $path, string $separator = '.') |
1159
|
|
|
{ |
1160
|
|
|
if (isset($array[$path])) { |
1161
|
|
|
return $array[$path]; |
1162
|
|
|
} |
1163
|
|
|
$keys = explode($separator, $path); |
1164
|
|
|
|
1165
|
|
|
foreach ($keys as $key) { |
1166
|
|
|
if (!array_key_exists($key, $array)) { |
1167
|
|
|
return; |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
|
|
$array = $array[$key]; |
1171
|
|
|
} |
1172
|
|
|
|
1173
|
|
|
return $array; |
1174
|
|
|
} |
1175
|
|
|
|
1176
|
|
|
/** |
1177
|
|
|
* Validate meta attributes. |
1178
|
|
|
*/ |
1179
|
|
|
protected function validateMetaAttributes(array $attributes): array |
1180
|
|
|
{ |
1181
|
|
|
foreach ($attributes as $attribute => $value) { |
1182
|
|
|
$const = __CLASS__.'::META_'.strtoupper($attribute); |
1183
|
|
|
if (!defined($const)) { |
1184
|
|
|
throw new Exception('meta attribute '.$attribute.' is not valid'); |
1185
|
|
|
} |
1186
|
|
|
|
1187
|
|
|
if ($attribute === NodeInterface::META_TAGS && !empty($value) && (!is_array($value) || array_filter($value, 'is_string') != $value)) { |
1188
|
|
|
throw new Exception('tags meta attribute must be an array of strings'); |
1189
|
|
|
} |
1190
|
|
|
|
1191
|
|
|
if ($attribute !== NodeInterface::META_TAGS && !is_string($value)) { |
1192
|
|
|
throw new Exception($attribute.' meta attribute must be a string'); |
1193
|
|
|
} |
1194
|
|
|
} |
1195
|
|
|
|
1196
|
|
|
return $attributes; |
1197
|
|
|
} |
1198
|
|
|
|
1199
|
|
|
/** |
1200
|
|
|
* Completly remove node. |
1201
|
|
|
*/ |
1202
|
|
|
abstract protected function _forceDelete(): bool; |
1203
|
|
|
} |
1204
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.