Completed
Branch dev (bc6e47)
by Raffael
02:23
created

File::getAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 15
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Balloon
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
10
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
11
 */
12
13
namespace Balloon\Filesystem\Node;
14
15
use Balloon\Exception;
16
use Balloon\Filesystem;
17
use Balloon\Filesystem\Acl;
18
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
19
use Balloon\Filesystem\Storage;
20
use Balloon\Helper;
21
use Balloon\Hook;
22
use Balloon\Mime;
23
use Balloon\Server\User;
24
use MongoDB\BSON\UTCDateTime;
25
use Psr\Log\LoggerInterface;
26
27
class File extends AbstractNode implements FileInterface
28
{
29
    /**
30
     * History types.
31
     */
32
    const HISTORY_CREATE = 0;
33
    const HISTORY_EDIT = 1;
34
    const HISTORY_RESTORE = 2;
35
    const HISTORY_DELETE = 3;
36
    const HISTORY_UNDELETE = 4;
37
38
    /**
39
     * Empty content hash (NULL).
40
     */
41
    const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e';
42
43
    /**
44
     * Temporary file patterns.
45
     *
46
     * @param array
47
     **/
48
    protected $temp_files = [
49
        '/^\._(.*)$/',     // OS/X resource forks
50
        '/^.DS_Store$/',   // OS/X custom folder settings
51
        '/^desktop.ini$/', // Windows custom folder settings
52
        '/^Thumbs.db$/',   // Windows thumbnail cache
53
        '/^.(.*).swpx$/',  // ViM temporary files
54
        '/^.(.*).swx$/',   // ViM temporary files
55
        '/^.(.*).swp$/',   // ViM temporary files
56
        '/^\.dat(.*)$/',   // Smultron seems to create these
57
        '/^~lock.(.*)#$/', // Windows 7 lockfiles
58
    ];
59
60
    /**
61
     * MD5 Hash of the content.
62
     *
63
     * @var string
64
     */
65
    protected $hash;
66
67
    /**
68
     * File version.
69
     *
70
     * @var int
71
     */
72
    protected $version = 0;
73
74
    /**
75
     * File size.
76
     *
77
     * @var int
78
     */
79
    protected $size = 0;
80
81
    /**
82
     * History.
83
     *
84
     * @var array
85
     */
86
    protected $history = [];
87
88
    /**
89
     * Storage.
90
     *
91
     * @var Storage
92
     */
93
    protected $_storage;
94
95
    /**
96
     * Storage attributes.
97
     *
98
     * @var mixed
99
     */
100
    protected $storage;
101
102
    /**
103
     * Initialize file node.
104
     *
105
     * @param array      $attributes
106
     * @param Filesystem $fs
107
     */
108
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Storage $storage)
109
    {
110
        $this->_fs = $fs;
111
        $this->_server = $fs->getServer();
112
        $this->_db = $fs->getDatabase();
113
        $this->_user = $fs->getUser();
114
        $this->_logger = $logger;
115
        $this->_hook = $hook;
116
        $this->_storage = $storage;
117
        $this->_acl = $acl;
118
119
        foreach ($attributes as $attr => $value) {
120
            $this->{$attr} = $value;
121
        }
122
123
        $this->raw_attributes = $attributes;
124
    }
125
126
    /**
127
     * Read content and return ressource.
128
     *
129
     * @return resource
130
     */
131
    public function get()
132
    {
133
        try {
134
            if (null === $this->storage) {
135
                return null;
136
            }
137
138
            return $this->_storage->getFile($this, $this->storage, $this->storage_adapter);
139
        } catch (\Exception $e) {
140
            throw new Exception\NotFound(
141
                'content not found',
142
                Exception\NotFound::CONTENTS_NOT_FOUND
143
            );
144
        }
145
    }
146
147
    /**
148
     * Copy node.
149
     *
150
     * @param Collection $parent
151
     * @param int        $conflict
152
     * @param string     $recursion
153
     * @param bool       $recursion_first
154
     *
155
     * @return NodeInterface
156
     */
157
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
158
    {
159
        $this->_hook->run(
160
            'preCopyFile',
161
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
162
        );
163
164
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
165
            $name = $this->getDuplicateName();
166
        } else {
167
            $name = $this->name;
168
        }
169
170
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
171
            $result = $parent->getChild($this->name);
172
            $result->put($this->get());
173
        } else {
174
            $result = $parent->addFile($name, $this->get(), [
175
                'created' => $this->created,
176
                'changed' => $this->changed,
177
                'deleted' => $this->deleted,
178
                'meta' => $this->meta,
179
            ], NodeInterface::CONFLICT_NOACTION, true);
180
        }
181
182
        $this->_hook->run(
183
            'postCopyFile',
184
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
185
        );
186
187
        return $result;
188
    }
189
190
    /**
191
     * Get history.
192
     *
193
     * @return array
194
     */
195
    public function getHistory(): array
196
    {
197
        $history = $this->history;
198
        $filtered = [];
199
200
        foreach ($history as $version) {
201
            $v = (array) $version;
202
203
            $v['user'] = $this->_fs->getServer()->getUserById($version['user'])->getUsername();
204
            $v['changed'] = Helper::DateTimeToUnix($version['changed']);
205
            $filtered[] = $v;
206
        }
207
208
        return $filtered;
209
    }
210
211
    /**
212
     * Restore content to some older version.
213
     *
214
     * @param int $version
215
     *
216
     * @return bool
217
     */
218
    public function restore(int $version): bool
219
    {
220
        if (!$this->_acl->isAllowed($this, 'w')) {
221
            throw new ForbiddenException(
222
                'not allowed to restore node '.$this->name,
223
                ForbiddenException::NOT_ALLOWED_TO_RESTORE
224
            );
225
        }
226
227
        $this->_hook->run('preRestoreFile', [$this, &$version]);
228
229
        if ($this->readonly) {
230
            throw new Exception\Conflict(
231
                'node is marked as readonly, it is not possible to change any content',
232
                Exception\Conflict::READONLY
233
            );
234
        }
235
236
        if ($this->version === $version) {
237
            throw new Exception('file is already version '.$version);
238
        }
239
240
        $v = array_search($version, array_column($this->history, 'version'), true);
241
        if (null === $v) {
242
            throw new Exception('failed restore file to version '.$version.', version was not found');
243
        }
244
245
        $file = $this->history[$v]['storage'];
246
        $exists = [];
247
248
        if (null !== $file) {
249
            try {
250
                $exists = $this->_storage->getFileMeta($this, $this->history[$v]['storage']);
251
            } catch (\Exception $e) {
252
                throw new Exception('could not restore to version '.$version.', version content does not exists');
253
            }
254
        }
255
256
        $current = $this->version;
0 ignored issues
show
Unused Code introduced by
$current 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...
257
        $new = $this->increaseVersion();
258
259
        $this->history[] = [
260
            'version' => $new,
261
            'changed' => $this->changed,
262
            'user' => $this->owner,
263
            'type' => self::HISTORY_RESTORE,
264
            'origin' => $this->history[$v]['version'],
265
            'storage' => $this->history[$v]['storage'],
266
            'storage_adapter' => $this->history[$v]['storage_adapter'],
267
            'size' => $this->history[$v]['size'],
268
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null,
269
        ];
270
271
        try {
272
            $this->deleted = false;
273
            $this->version = $new;
274
            $this->storage = $this->history[$v]['storage'];
275
            $this->storage_adapter = $this->history[$v]['storage_adapter'];
276
277
            $this->hash = null === $file ? self::EMPTY_CONTENT : $exists['md5'];
278
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null;
279
            $this->size = $this->history[$v]['size'];
280
            $this->changed = $this->history[$v]['changed'];
281
282
            $this->save([
283
                'deleted',
284
                'version',
285
                'storage',
286
                'storage_adapter',
287
                'hash',
288
                'mime',
289
                'size',
290
                'history',
291
                'changed',
292
            ]);
293
294
            $this->_hook->run('postRestoreFile', [$this, &$version]);
295
296
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
297
                'category' => get_class($this),
298
            ]);
299
        } catch (\Exception $e) {
300
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
301
                'category' => get_class($this),
302
                'exception' => $e,
303
            ]);
304
305
            throw $e;
306
        }
307
308
        return true;
309
    }
310
311
    /**
312
     * Delete node.
313
     *
314
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
315
     * delete finally
316
     *
317
     * @param bool   $force
318
     * @param string $recursion
319
     * @param bool   $recursion_first
320
     *
321
     * @return bool
322
     */
323
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
324
    {
325
        if (!$this->_acl->isAllowed($this, 'w')) {
326
            throw new ForbiddenException(
327
                'not allowed to delete node '.$this->name,
328
                ForbiddenException::NOT_ALLOWED_TO_DELETE
329
            );
330
        }
331
332
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
333
334
        if ($this->readonly && null !== $this->_user) {
335
            throw new Exception\Conflict(
336
                'node is marked as readonly, it is not possible to delete it',
337
                Exception\Conflict::READONLY
338
            );
339
        }
340
341
        if (true === $force || $this->isTemporaryFile()) {
342
            $result = $this->_forceDelete();
343
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
344
345
            return $result;
346
        }
347
348
        $ts = new UTCDateTime();
349
        $this->deleted = $ts;
350
        $this->increaseVersion();
351
352
        $this->history[] = [
353
            'version' => $this->version,
354
            'changed' => $ts,
355
            'user' => $this->_user->getId(),
356
            'type' => self::HISTORY_DELETE,
357
            'storage' => $this->storage,
358
            'storage_adapter' => $this->storage_adapter,
359
            'size' => $this->size,
360
        ];
361
362
        $result = $this->save([
363
            'version',
364
            'deleted',
365
            'history',
366
        ], [], $recursion, $recursion_first);
367
368
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
369
370
        return $result;
371
    }
372
373
    /**
374
     * Check if file is temporary.
375
     *
376
     * @return bool
377
     **/
378
    public function isTemporaryFile(): bool
379
    {
380
        foreach ($this->temp_files as $pattern) {
381
            if (preg_match($pattern, $this->name)) {
382
                return true;
383
            }
384
        }
385
386
        return false;
387
    }
388
389
    /**
390
     * Delete version.
391
     *
392
     * @param int $version
393
     *
394
     * @return bool
395
     */
396
    public function deleteVersion(int $version): bool
397
    {
398
        $key = array_search($version, array_column($this->history, 'version'), true);
399
400
        if (false === $key) {
401
            throw new Exception('version '.$version.' does not exists');
402
        }
403
404
        try {
405
            if ($this->history[$key]['storage'] !== null) {
406
                $this->_storage->deleteFile($this, $this->history[$key]['storage']);
407
            }
408
409
            array_splice($this->history, $key, 1);
410
411
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
412
                'category' => get_class($this),
413
            ]);
414
415
            return $this->save('history');
416
        } catch (\Exception $e) {
417
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
418
                'category' => get_class($this),
419
                'exception' => $e,
420
            ]);
421
422
            throw $e;
423
        }
424
    }
425
426
    /**
427
     * Cleanup history.
428
     *
429
     * @return bool
430
     */
431
    public function cleanHistory(): bool
432
    {
433
        foreach ($this->history as $node) {
434
            $this->deleteVersion($node['version']);
435
        }
436
437
        return true;
438
    }
439
440
    /**
441
     * Get Attributes.
442
     *
443
     * @return array
444
     */
445
    public function getAttributes(): array
446
    {
447
        return [
448
            'id' => $this->_id,
449
            'name' => $this->name,
450
            'hash' => $this->hash,
451
            'size' => $this->size,
452
            'version' => $this->version,
453
            'parent' => $this->parent,
454
            'meta' => $this->meta,
455
            'mime' => $this->mime,
456
            'deleted' => $this->deleted,
457
            'changed' => $this->changed,
458
            'created' => $this->created,
459
            'destroy' => $this->destroy,
460
            'readonly' => $this->readonly,
461
        ];
462
    }
463
464
    /**
465
     * Get filename extension.
466
     *
467
     * @return string
468
     */
469
    public function getExtension(): string
470
    {
471
        $ext = strrchr($this->name, '.');
472
        if (false === $ext) {
473
            throw new Exception('file does not have an extension');
474
        }
475
476
        return substr($ext, 1);
477
    }
478
479
    /**
480
     * Get file size.
481
     *
482
     * @return int
483
     */
484
    public function getSize(): int
485
    {
486
        return $this->size;
487
    }
488
489
    /**
490
     * Get md5 sum of the file content,
491
     * actually the hash value comes from the database.
492
     *
493
     * @return string
494
     */
495
    public function getETag(): string
496
    {
497
        return "'".$this->hash."'";
498
    }
499
500
    /**
501
     * Get hash.
502
     *
503
     * @return string
504
     */
505
    public function getHash(): string
506
    {
507
        return $this->hash;
508
    }
509
510
    /**
511
     * Get version.
512
     *
513
     * @return int
514
     */
515
    public function getVersion(): int
516
    {
517
        return $this->version;
518
    }
519
520
    /**
521
     * Change content.
522
     *
523
     * @param resource|string $file
524
     * @param bool            $new
525
     * @param array           $attributes
526
     *
527
     * @return int
528
     */
529
    public function put($file, bool $new = false, array $attributes = []): int
530
    {
531
        $this->_logger->debug('add contents for file ['.$this->_id.']', [
532
            'category' => get_class($this),
533
        ]);
534
535
        $this->validatePutRequest($file, $new, $attributes);
536
        $file = $this->createTemporaryFile($file, $stream);
537
        $new_hash = $this->verifyFile($file, $new);
538
539
        if ($this->hash === $new_hash) {
540
            $this->_logger->info('stop PUT execution, content checksums are equal for file ['.$this->_id.']', [
541
                'category' => get_class($this),
542
            ]);
543
544
            //Remove tmp file
545
            if (null !== $file) {
546
                unlink($file);
547
                fclose($stream);
548
            }
549
550
            return $this->version;
551
        }
552
553
        $this->hash = $new_hash;
554
        $max = (int) (string) $this->_fs->getServer()->getMaxFileVersion();
555
        if (count($this->history) >= $max) {
556
            $del = key($this->history);
557
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$del.'] from file ['.$this->_id.']', [
558
                'category' => get_class($this),
559
            ]);
560
561
            $this->deleteVersion($this->history[$del]['version']);
562
        }
563
564
        //Write new content
565
        if ($this->size > 0) {
566
            $this->storage = $this->_storage->storeFile($this, $stream, $this->storage_adapter);
567
        } else {
568
            $this->storage = null;
569
        }
570
571
        //Update current version
572
        $this->increaseVersion();
573
574
        //Get meta attributes
575
        if (isset($attributes['mime'])) {
576
            $this->mime = $attributes['mime'];
577
        } elseif (null !== $file) {
578
            $this->mime = (new Mime())->getMime($file, $this->name);
579
        }
580
581
        //Remove tmp file
582
        if (null !== $file) {
583
            unlink($file);
584
            fclose($stream);
585
        }
586
587
        $this->_logger->debug('set mime ['.$this->mime.'] for content, file=['.$this->_id.']', [
588
            'category' => get_class($this),
589
        ]);
590
591
        $this->addVersion($attributes)
592
             ->postPutFile($file, $new, $attributes);
593
594
        return $this->version;
595
    }
596
597
    /**
598
     * Completly remove file.
599
     *
600
     * @return bool
601
     */
602
    protected function _forceDelete(): bool
603
    {
604
        try {
605
            $this->cleanHistory();
606
            $this->_db->storage->deleteOne([
607
                '_id' => $this->_id,
608
            ]);
609
610
            $this->_logger->info('removed file node ['.$this->_id.']', [
611
                'category' => get_class($this),
612
            ]);
613
        } catch (\Exception $e) {
614
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
615
                'category' => get_class($this),
616
                'exception' => $e,
617
            ]);
618
619
            throw $e;
620
        }
621
622
        return true;
623
    }
624
625
    /**
626
     * Increase version.
627
     *
628
     * @return int
629
     */
630
    protected function increaseVersion(): int
631
    {
632
        ++$this->version;
633
634
        return $this->version;
635
    }
636
637
    /**
638
     * Create uuidv4.
639
     *
640
     * @param string $data
641
     *
642
     * @return string
643
     */
644
    protected function guidv4(string $data): string
645
    {
646
        assert(16 === strlen($data));
647
648
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
649
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
650
651
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
652
    }
653
654
    /**
655
     * Change content.
656
     *
657
     * @param resource|string $file
658
     * @param bool            $new
659
     * @param array           $attributes
660
     *
661
     * @return bool
662
     */
663
    protected function validatePutRequest($file, bool $new = false, array $attributes = []): bool
664
    {
665
        if (!$this->_acl->isAllowed($this, 'w')) {
666
            throw new ForbiddenException(
667
                'not allowed to modify node',
668
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
669
            );
670
        }
671
672
        $this->_hook->run('prePutFile', [$this, &$file, &$new, &$attributes]);
673
674
        if ($this->readonly) {
675
            throw new Exception\Conflict(
676
                'node is marked as readonly, it is not possible to change any content',
677
                Exception\Conflict::READONLY
678
            );
679
        }
680
681
        if ($this->isShareMember() && false === $new && 'w' === $this->_acl->getAclPrivilege($this->getShareNode())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getShareNode() can be null; however, getAclPrivilege() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
682
            throw new ForbiddenException(
683
                'not allowed to overwrite node',
684
                ForbiddenException::NOT_ALLOWED_TO_OVERWRITE
685
            );
686
        }
687
688
        return true;
689
    }
690
691
    /**
692
     * Verify content to be added.
693
     *
694
     * @param string $path
695
     * @param bool   $new
696
     *
697
     * @return bool
698
     */
699
    protected function verifyFile(?string $path, bool $new = false): string
700
    {
701
        if (null === $path) {
702
            $this->size = 0;
703
            $new_hash = self::EMPTY_CONTENT;
704
        } else {
705
            $size = filesize($path);
706
            $this->size = $size;
707
            $new_hash = md5_file($path);
708
709
            if (!$this->_user->checkQuota($size)) {
710
                $this->_logger->warning('could not execute PUT, user quota is full', [
711
                    'category' => get_class($this),
712
                ]);
713
714
                if (true === $new) {
715
                    $this->_forceDelete();
716
                }
717
718
                throw new Exception\InsufficientStorage(
719
                    'user quota is full',
720
                    Exception\InsufficientStorage::USER_QUOTA_FULL
721
                );
722
            }
723
        }
724
725
        return $new_hash;
726
    }
727
728
    /**
729
     * Create temporary file.
730
     *
731
     * @param resource|string $file
732
     * @param resource        $stream
733
     *
734
     * @return string
735
     */
736
    protected function createTemporaryFile($file, &$stream): ?string
737
    {
738
        if (is_string($file)) {
739
            if (!is_readable($file)) {
740
                throw new Exception('file does not exists or is not readable');
741
            }
742
743
            $stream = fopen($file, 'r');
744
        } elseif (is_resource($file)) {
745
            $tmp = $this->_fs->getServer()->getTempDir().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.$this->_user->getId();
746
            if (!file_exists($tmp)) {
747
                mkdir($tmp, 0700, true);
748
            }
749
750
            $tmp_file = $tmp.DIRECTORY_SEPARATOR.$this->guidv4(openssl_random_pseudo_bytes(16));
751
            $stream = fopen($tmp_file, 'w+');
752
            $size = stream_copy_to_stream($file, $stream, ((int) $this->_fs->getServer()->getMaxFileSize() + 1));
753
            rewind($stream);
754
            fclose($file);
755
756
            if ($size > (int) $this->_fs->getServer()->getMaxFileSize()) {
757
                unlink($tmp_file);
758
759
                throw new Exception\InsufficientStorage(
760
                    'file size exceeded limit',
761
                    Exception\InsufficientStorage::FILE_SIZE_LIMIT
762
                );
763
            }
764
765
            $file = $tmp_file;
766
        } else {
767
            $file = null;
768
        }
769
770
        return $file;
771
    }
772
773
    /**
774
     * Add new version.
775
     *
776
     * @param array $attributes
777
     *
778
     * @return File
779
     */
780
    protected function addVersion(array $attributes = []): self
781
    {
782
        if (1 !== $this->version) {
783
            if (isset($attributes['changed'])) {
784
                if (!($attributes['changed'] instanceof UTCDateTime)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
785
                    throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
786
                }
787
788
                $this->changed = $attributes['changed'];
789
            } else {
790
                $this->changed = new UTCDateTime();
791
            }
792
793
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
794
                'category' => get_class($this),
795
            ]);
796
797
            $this->history[] = [
798
                'version' => $this->version,
799
                'changed' => $this->changed,
800
                'user' => $this->_user->getId(),
801
                'type' => self::HISTORY_EDIT,
802
                'storage' => $this->storage,
803
                'storage_adapter' => $this->storage_adapter,
804
                'size' => $this->size,
805
                'mime' => $this->mime,
806
            ];
807
        } else {
808
            $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
809
                'category' => get_class($this),
810
            ]);
811
812
            $this->history[0] = [
813
                'version' => 1,
814
                'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
815
                'user' => $this->owner,
816
                'type' => self::HISTORY_CREATE,
817
                'storage' => $this->storage,
818
                'storage_adapter' => $this->storage_adapter,
819
                'size' => $this->size,
820
                'mime' => $this->mime,
821
            ];
822
        }
823
824
        return $this;
825
    }
826
827
    /**
828
     * Finalize put request.
829
     *
830
     * @param resource|string $file
831
     * @param bool            $new
832
     * @param array           $attributes
833
     *
834
     * @return File
835
     */
836
    protected function postPutFile($file, bool $new, array $attributes): self
837
    {
838
        try {
839
            $this->save([
840
                'size',
841
                'changed',
842
                'mime',
843
                'hash',
844
                'version',
845
                'history',
846
                'storage',
847
            ]);
848
849
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
850
                'category' => get_class($this),
851
            ]);
852
853
            $this->_hook->run('postPutFile', [$this, $file, $new, $attributes]);
854
855
            return $this;
856
        } catch (\Exception $e) {
857
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
858
                'category' => get_class($this),
859
                'exception' => $e,
860
            ]);
861
862
            throw $e;
863
        }
864
    }
865
}
866