Completed
Push — master ( afaf42...8ac772 )
by Raffael
20:10 queued 16:21
created

File::setContent()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
dl 0
loc 49
ccs 0
cts 27
cp 0
rs 7.8682
c 0
b 0
f 0
cc 8
nc 11
nop 2
crap 72
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception as AclException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Filesystem\Storage\Exception as StorageException;
19
use Balloon\Hook;
20
use MongoDB\BSON\ObjectId;
21
use MongoDB\BSON\UTCDateTime;
22
use Psr\Log\LoggerInterface;
23
use Sabre\DAV\IFile;
24
25
class File extends AbstractNode implements IFile
26
{
27
    /**
28
     * History types.
29
     */
30
    const HISTORY_CREATE = 0;
31
    const HISTORY_EDIT = 1;
32
    const HISTORY_RESTORE = 2;
33
34
    /**
35
     * Empty content hash (NULL).
36
     */
37
    const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e';
38
39
    /**
40
     * Temporary file patterns.
41
     *
42
     * @param array
43
     **/
44
    protected $temp_files = [
45
        '/^\._(.*)$/',     // OS/X resource forks
46
        '/^.DS_Store$/',   // OS/X custom folder settings
47
        '/^desktop.ini$/', // Windows custom folder settings
48
        '/^Thumbs.db$/',   // Windows thumbnail cache
49
        '/^.(.*).swpx$/',  // ViM temporary files
50
        '/^.(.*).swx$/',   // ViM temporary files
51
        '/^.(.*).swp$/',   // ViM temporary files
52
        '/^\.dat(.*)$/',   // Smultron seems to create these
53
        '/^~lock.(.*)#$/', // Windows 7 lockfiles
54
    ];
55
56
    /**
57
     * MD5 Hash of the content.
58
     *
59
     * @var string
60
     */
61
    protected $hash;
62
63
    /**
64
     * File version.
65
     *
66
     * @var int
67
     */
68
    protected $version = 0;
69
70
    /**
71
     * File size.
72
     *
73
     * @var int
74
     */
75
    protected $size = 0;
76
77
    /**
78
     * History.
79
     *
80
     * @var array
81
     */
82
    protected $history = [];
83
84
    /**
85
     * Initialize file node.
86
     */
87 10
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Collection $parent)
88
    {
89 10
        $this->_fs = $fs;
90 10
        $this->_server = $fs->getServer();
91 10
        $this->_db = $fs->getDatabase();
92 10
        $this->_user = $fs->getUser();
93 10
        $this->_logger = $logger;
94 10
        $this->_hook = $hook;
95 10
        $this->_acl = $acl;
96 10
        $this->_parent = $parent;
97
98 10
        foreach ($attributes as $attr => $value) {
99 10
            $this->{$attr} = $value;
100
        }
101
102 10
        $this->raw_attributes = $attributes;
103 10
    }
104
105
    /**
106
     * Read content and return ressource.
107
     *
108
     * @return resource
109
     */
110
    public function get()
111
    {
112
        if (null === $this->storage) {
113
            return null;
114
        }
115
116
        try {
117
            return $this->_parent->getStorage()->openReadStream($this);
118
        } catch (\Exception $e) {
119
            throw new Exception\NotFound(
120
                'storage blob is gone',
121
                Exception\NotFound::CONTENTS_NOT_FOUND,
122
                $e
123
            );
124
        }
125
    }
126
127
    /**
128
     * Copy node.
129
     *
130
     * @param string $recursion
131
     */
132
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
133
    {
134
        $this->_hook->run(
135
            'preCopyFile',
136
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
137
        );
138
139
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
140
            $name = $this->getDuplicateName();
141
        } else {
142
            $name = $this->name;
143
        }
144
145
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
146
            $result = $parent->getChild($this->name);
147
            $result->put($this->get());
148
        } else {
149
            $stream = $this->get();
150
            $session = $parent->getStorage()->storeTemporaryFile($stream, $this->_server->getUserById($this->getOwner()));
151
            $result = $parent->addFile($name, $session, [
152
                'created' => $this->created,
153
                'changed' => $this->changed,
154
                'deleted' => $this->deleted,
155
                'meta' => $this->meta,
156
            ], NodeInterface::CONFLICT_NOACTION, true);
157
158
            fclose($stream);
159
        }
160
161
        $this->_hook->run(
162
            'postCopyFile',
163
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
164
        );
165
166
        return $result;
167
    }
168
169
    /**
170
     * Get history.
171
     */
172
    public function getHistory(): array
173
    {
174
        return $this->history;
175
    }
176
177
    /**
178
     * Restore content to some older version.
179
     */
180
    public function restore(int $version): bool
181
    {
182
        if (!$this->_acl->isAllowed($this, 'w')) {
183
            throw new AclException\Forbidden(
184
                'not allowed to restore node '.$this->name,
185
                AclException\Forbidden::NOT_ALLOWED_TO_RESTORE
186
            );
187
        }
188
189
        $this->_hook->run('preRestoreFile', [$this, &$version]);
190
191
        if ($this->readonly) {
192
            throw new Exception\Conflict(
193
                'node is marked as readonly, it is not possible to change any content',
194
                Exception\Conflict::READONLY
195
            );
196
        }
197
198
        if ($this->version === $version) {
199
            throw new Exception('file is already version '.$version);
200
        }
201
202
        $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...
203
        $new = $this->increaseVersion();
204
205
        $v = array_search($version, array_column($this->history, 'version'), true);
206
        if (false === $v) {
207
            throw new Exception('failed restore file to version '.$version.', version was not found');
208
        }
209
210
        $file = $this->history[$v]['storage'];
211
212
        $this->history[] = [
213
            'version' => $new,
214
            'changed' => $this->changed,
215
            'user' => $this->owner,
216
            'type' => self::HISTORY_RESTORE,
217
            'hash' => $this->history[$v]['hash'],
218
            'origin' => $this->history[$v]['version'],
219
            'storage' => $this->history[$v]['storage'],
220
            'storage_reference' => $this->history[$v]['storage_reference'],
221
            'size' => $this->history[$v]['size'],
222
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null,
223
        ];
224
225
        try {
226
            $this->deleted = false;
227
            $this->version = $new;
228
            $this->storage = $this->history[$v]['storage'];
229
            $this->storage_reference = $this->history[$v]['storage_reference'];
230
231
            $this->hash = null === $file ? self::EMPTY_CONTENT : $this->history[$v]['hash'];
232
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null;
233
            $this->size = $this->history[$v]['size'];
234
            $this->changed = $this->history[$v]['changed'];
235
236
            $this->save([
237
                'deleted',
238
                'version',
239
                'storage',
240
                'storage_reference',
241
                'hash',
242
                'mime',
243
                'size',
244
                'history',
245
                'changed',
246
            ]);
247
248
            $this->_hook->run('postRestoreFile', [$this, &$version]);
249
250
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
251
                'category' => get_class($this),
252
            ]);
253
        } catch (\Exception $e) {
254
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
255
                'category' => get_class($this),
256
                'exception' => $e,
257
            ]);
258
259
            throw $e;
260
        }
261
262
        return true;
263
    }
264
265
    /**
266
     * Delete node.
267
     *
268
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
269
     * delete finally
270
     *
271
     * @param string $recursion
272
     */
273
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
274
    {
275
        if (!$this->_acl->isAllowed($this, 'w')) {
276
            throw new AclException\Forbidden(
277
                'not allowed to delete node '.$this->name,
278
                AclException\Forbidden::NOT_ALLOWED_TO_DELETE
279
            );
280
        }
281
282
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
283
284
        if (true === $force || $this->isTemporaryFile()) {
285
            $result = $this->_forceDelete();
286
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
287
288
            return $result;
289
        }
290
291
        $ts = new UTCDateTime();
292
        $this->deleted = $ts;
293
        $this->storage = $this->_parent->getStorage()->deleteFile($this);
294
295
        $result = $this->save([
296
            'version',
297
            'storage',
298
            'deleted',
299
            'history',
300
        ], [], $recursion, $recursion_first);
301
302
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
303
304
        return $result;
305
    }
306
307
    /**
308
     * Check if file is temporary.
309
     */
310 2
    public function isTemporaryFile(): bool
311
    {
312 2
        foreach ($this->temp_files as $pattern) {
313 2
            if (preg_match($pattern, $this->name)) {
314 2
                return true;
315
            }
316
        }
317
318 1
        return false;
319
    }
320
321
    /**
322
     * Delete version.
323
     */
324
    public function deleteVersion(int $version): bool
325
    {
326
        $key = array_search($version, array_column($this->history, 'version'), true);
327
328
        if (false === $key) {
329
            throw new Exception('version '.$version.' does not exists');
330
        }
331
332
        $blobs = array_column($this->history, 'storage');
333
334
        try {
335
            //do not remove blob if there are other versions linked against it
336
            if ($this->history[$key]['storage'] !== null && count(array_keys($blobs, $this->history[$key]['storage'])) === 1) {
337
                $this->_parent->getStorage()->forceDeleteFile($this, $version);
338
            }
339
340
            array_splice($this->history, $key, 1);
341
342
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
343
                'category' => get_class($this),
344
            ]);
345
346
            return $this->save('history');
347
        } catch (StorageException\BlobNotFound $e) {
348
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
349
                'category' => get_class($this),
350
                'exception' => $e,
351
            ]);
352
353
            return false;
354
        }
355
    }
356
357
    /**
358
     * Cleanup history.
359
     */
360
    public function cleanHistory(): bool
361
    {
362
        foreach ($this->history as $node) {
363
            $this->deleteVersion($node['version']);
364
        }
365
366
        return true;
367
    }
368
369
    /**
370
     * Get Attributes.
371
     */
372
    public function getAttributes(): array
373
    {
374
        return [
375
            '_id' => $this->_id,
376
            'name' => $this->name,
377
            'hash' => $this->hash,
378
            'directory' => false,
379
            'size' => $this->size,
380
            'version' => $this->version,
381
            'parent' => $this->parent,
382
            'acl' => $this->acl,
383
            'app' => $this->app,
384
            'meta' => $this->meta,
385
            'mime' => $this->mime,
386
            'owner' => $this->owner,
387
            'history' => $this->history,
388
            'shared' => $this->shared,
389
            'deleted' => $this->deleted,
390
            'changed' => $this->changed,
391
            'created' => $this->created,
392
            'destroy' => $this->destroy,
393
            'readonly' => $this->readonly,
394
            'storage_reference' => $this->storage_reference,
395
            'storage' => $this->storage,
396
        ];
397
    }
398
399
    /**
400
     * Get filename extension.
401
     */
402 2
    public function getExtension(): string
403
    {
404 2
        $ext = strrchr($this->name, '.');
405 2
        if (false === $ext) {
406 1
            throw new Exception('file does not have an extension');
407
        }
408
409 1
        return substr($ext, 1);
410
    }
411
412
    /**
413
     * Get file size.
414
     */
415 1
    public function getSize(): int
416
    {
417 1
        return $this->size;
418
    }
419
420
    /**
421
     * Get md5 sum of the file content,
422
     * actually the hash value comes from the database.
423
     */
424 1
    public function getETag(): string
425
    {
426 1
        return "'".$this->hash."'";
427
    }
428
429
    /**
430
     * Get hash.
431
     *
432
     * @return string
433
     */
434 1
    public function getHash(): ?string
435
    {
436 1
        return $this->hash;
437
    }
438
439
    /**
440
     * Get version.
441
     */
442 1
    public function getVersion(): int
443
    {
444 1
        return $this->version;
445
    }
446
447
    /**
448
     * Change content (Sabe dav compatible method).
449
     */
450
    public function put($content): int
451
    {
452
        $this->_logger->debug('write new file content into temporary storage for file ['.$this->_id.']', [
453
            'category' => get_class($this),
454
        ]);
455
456
        $session = $this->_parent->getStorage()->storeTemporaryFile($content, $this->_user);
457
458
        return $this->setContent($session);
459
    }
460
461
    /**
462
     * Set content (temporary file).
463
     */
464
    public function setContent(ObjectId $session, array $attributes = []): int
465
    {
466
        $this->_logger->debug('set temporary file ['.$session.'] as file content for ['.$this->_id.']', [
467
            'category' => get_class($this),
468
        ]);
469
470
        $previous = $this->version;
471
        $storage = $this->storage;
472
        $this->prePutFile($session);
473
        $result = $this->_parent->getStorage()->storeFile($this, $session);
474
        $this->storage = $result['reference'];
475
476
        if ($this->hash === $result['hash']) {
477
            $this->_logger->debug('do not update file version, hash identical to existing version ['.$this->hash.' == '.$result['hash'].']', [
478
                'category' => get_class($this),
479
            ]);
480
481
            return $this->version;
482
        }
483
484
        $this->hash = $result['hash'];
485
        $this->size = $result['size'];
486
487
        if ($this->size === 0 && $this->getMount() === null) {
488
            $this->storage = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $storage.

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

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

Loading history...
489
        } else {
490
            $this->storage = $result['reference'];
491
        }
492
493
        $this->increaseVersion();
494
495
        if (isset($attributes['changed'])) {
496
            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...
497
                throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
498
            }
499
500
            $this->changed = $attributes['changed'];
501
        } else {
502
            $this->changed = new UTCDateTime();
503
        }
504
505
        if ($result['reference'] != $storage || $previous === 0) {
506
            $this->addVersion($attributes);
507
        }
508
509
        $this->postPutFile();
510
511
        return $this->version;
512
    }
513
514
    /**
515
     * Completly remove file.
516
     */
517
    protected function _forceDelete(): bool
518
    {
519
        try {
520
            $this->_parent->getStorage()->forceDeleteFile($this);
521
            $this->cleanHistory();
522
            $this->_db->storage->deleteOne([
523
                '_id' => $this->_id,
524
            ]);
525
526
            $this->_logger->info('removed file node ['.$this->_id.']', [
527
                'category' => get_class($this),
528
            ]);
529
        } catch (\Exception $e) {
530
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
531
                'category' => get_class($this),
532
                'exception' => $e,
533
            ]);
534
535
            throw $e;
536
        }
537
538
        return true;
539
    }
540
541
    /**
542
     * Increase version.
543
     */
544
    protected function increaseVersion(): int
545
    {
546
        $max = $this->_fs->getServer()->getMaxFileVersion();
547
        if (count($this->history) >= $max) {
548
            $del = key($this->history);
549
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$del.'] from file ['.$this->_id.']', [
550
                'category' => get_class($this),
551
            ]);
552
553
            $this->deleteVersion($this->history[$del]['version']);
554
        }
555
556
        ++$this->version;
557
558
        return $this->version;
559
    }
560
561
    /**
562
     * Pre content change checks.
563
     */
564
    protected function prePutFile(ObjectId $session): bool
565
    {
566
        if (!$this->_acl->isAllowed($this, 'w')) {
567
            throw new AclException\Forbidden(
568
                'not allowed to modify node',
569
                AclException\Forbidden::NOT_ALLOWED_TO_MODIFY
570
            );
571
        }
572
573
        $this->_hook->run('prePutFile', [$this, &$session]);
574
575
        if ($this->readonly) {
576
            throw new Exception\Conflict(
577
                'node is marked as readonly, it is not possible to change any content',
578
                Exception\Conflict::READONLY
579
            );
580
        }
581
582
        return true;
583
    }
584
585
    /**
586
     * Add new version.
587
     */
588
    protected function addVersion(array $attributes = []): self
589
    {
590
        if (1 !== $this->version) {
591
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
592
                'category' => get_class($this),
593
            ]);
594
595
            $this->history[] = [
596
                'version' => $this->version,
597
                'changed' => $this->changed,
598
                'user' => $this->_user->getId(),
599
                'type' => self::HISTORY_EDIT,
600
                'storage' => $this->storage,
601
                'storage_reference' => $this->storage_reference,
602
                'size' => $this->size,
603
                'mime' => $this->mime,
604
                'hash' => $this->hash,
605
            ];
606
607
            return $this;
608
        }
609
610
        $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
611
            'category' => get_class($this),
612
        ]);
613
614
        $this->history[0] = [
615
            'version' => 1,
616
            'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
617
            'user' => $this->owner,
618
            'type' => self::HISTORY_CREATE,
619
            'storage' => $this->storage,
620
            'storage_reference' => $this->storage_reference,
621
            'size' => $this->size,
622
            'mime' => $this->mime,
623
            'hash' => $this->hash,
624
        ];
625
626
        return $this;
627
    }
628
629
    /**
630
     * Finalize put request.
631
     */
632
    protected function postPutFile(): self
633
    {
634
        try {
635
            $this->save([
636
                'size',
637
                'changed',
638
                'mime',
639
                'hash',
640
                'version',
641
                'history',
642
                'storage',
643
                'storage_reference',
644
            ]);
645
646
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
647
                'category' => get_class($this),
648
            ]);
649
650
            $this->_hook->run('postPutFile', [$this]);
651
652
            return $this;
653
        } catch (\Exception $e) {
654
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
655
                'category' => get_class($this),
656
                'exception' => $e,
657
            ]);
658
659
            throw $e;
660
        }
661
    }
662
}
663