Completed
Pull Request — master (#115)
by
unknown
02:18
created

Server::getSupportedHashAlgorithms()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 14
ccs 0
cts 8
cp 0
crap 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace TusPhp\Tus;
4
5
use TusPhp\File;
6
use Carbon\Carbon;
7
use TusPhp\Request;
8
use TusPhp\Response;
9
use Ramsey\Uuid\Uuid;
10
use TusPhp\Cache\Cacheable;
11
use TusPhp\Events\UploadMerged;
12
use TusPhp\Events\UploadCreated;
13
use TusPhp\Events\UploadComplete;
14
use TusPhp\Events\UploadProgress;
15
use TusPhp\Middleware\Middleware;
16
use TusPhp\Exception\FileException;
17
use TusPhp\Exception\ConnectionException;
18
use TusPhp\Exception\OutOfRangeException;
19
use Symfony\Component\HttpFoundation\BinaryFileResponse;
20
use Symfony\Component\HttpFoundation\Response as HttpResponse;
21
22
class Server extends AbstractTus
23
{
24
    /** @const string Tus Creation Extension */
25
    const TUS_EXTENSION_CREATION = 'creation';
26
27
    /** @const string Tus Termination Extension */
28
    const TUS_EXTENSION_TERMINATION = 'termination';
29
30
    /** @const string Tus Checksum Extension */
31
    const TUS_EXTENSION_CHECKSUM = 'checksum';
32
33
    /** @const string Tus Expiration Extension */
34
    const TUS_EXTENSION_EXPIRATION = 'expiration';
35
36
    /** @const string Tus Concatenation Extension */
37
    const TUS_EXTENSION_CONCATENATION = 'concatenation';
38
39
    /** @const array All supported tus extensions */
40
    const TUS_EXTENSIONS = [
41
        self::TUS_EXTENSION_CREATION,
42
        self::TUS_EXTENSION_TERMINATION,
43
        self::TUS_EXTENSION_CHECKSUM,
44
        self::TUS_EXTENSION_EXPIRATION,
45
        self::TUS_EXTENSION_CONCATENATION,
46
    ];
47
48
    /** @const int 460 Checksum Mismatch */
49
    const HTTP_CHECKSUM_MISMATCH = 460;
50
51
    /** @const string Default checksum algorithm */
52
    const DEFAULT_CHECKSUM_ALGORITHM = 'sha256';
53
54
    /** @var Request */
55
    protected $request;
56
57
    /** @var Response */
58
    protected $response;
59
60
    /** @var string */
61
    protected $uploadDir;
62
63
    /** @var string */
64
    protected $uploadKey;
65
66
    /** @var Middleware */
67
    protected $middleware;
68
69
    /**
70
     * @var int Max upload size in bytes
71
     *          Default 0, no restriction.
72
     */
73
    protected $maxUploadSize = 0;
74
75
    /**
76
     * TusServer constructor.
77
     *
78
     * @param Cacheable|string $cacheAdapter
79
     *
80
     * @throws \ReflectionException
81
     */
82
    public function __construct($cacheAdapter = 'file')
83
    {
84
        $this->request    = new Request;
85
        $this->response   = new Response;
86
        $this->middleware = new Middleware;
87
        $this->uploadDir  = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'uploads';
88
89
        $this->setCache($cacheAdapter);
90
    }
91
92
    /**
93
     * Set upload dir.
94
     *
95
     * @param string $path
96
     *
97
     * @return Server
98
     */
99
    public function setUploadDir(string $path) : self
100
    {
101
        $this->uploadDir = $path;
102
103
        return $this;
104
    }
105
106
    /**
107
     * Get upload dir.
108
     *
109
     * @return string
110
     */
111
    public function getUploadDir() : string
112
    {
113
        return $this->uploadDir;
114
    }
115
116
    /**
117
     * Get request.
118
     *
119
     * @return Request
120
     */
121
    public function getRequest() : Request
122
    {
123
        return $this->request;
124
    }
125
126
    /**
127
     * Get request.
128
     *
129
     * @return Response
130
     */
131
    public function getResponse() : Response
132
    {
133
        return $this->response;
134
    }
135
136
    /**
137
     * Get file checksum.
138
     *
139
     * @param string $filePath
140
     *
141
     * @return string
142
     */
143
    public function getServerChecksum(string $filePath) : string
144
    {
145
        return hash_file($this->getChecksumAlgorithm(), $filePath);
146
    }
147
148
    /**
149
     * Get checksum algorithm.
150
     *
151
     * @return string|null
152
     */
153
    public function getChecksumAlgorithm() : ?string
154
    {
155
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
156
157
        if (empty($checksumHeader)) {
158
            return self::DEFAULT_CHECKSUM_ALGORITHM;
159
        }
160
161
        list($checksumAlgorithm, /* $checksum */) = explode(' ', $checksumHeader);
162
163
        return $checksumAlgorithm;
164
    }
165
166
    /**
167
     * Set upload key.
168
     *
169
     * @param string $key
170
     *
171
     * @return Server
172
     */
173
    public function setUploadKey(string $key) : self
174
    {
175
        $this->uploadKey = $key;
176
177
        return $this;
178
    }
179
180
    /**
181
     * Get upload key from header.
182
     *
183
     * @return string|HttpResponse
184
     */
185
    public function getUploadKey()
186
    {
187
        if ( ! empty($this->uploadKey)) {
188
            return $this->uploadKey;
189
        }
190
191
        $key = $this->getRequest()->header('Upload-Key') ?? Uuid::uuid4()->toString();
192
193
        if (empty($key)) {
194
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
195
        }
196
197
        $this->uploadKey = $key;
198
199
        return $this->uploadKey;
200
    }
201
202
    /**
203
     * Set middleware.
204
     *
205
     * @param Middleware $middleware
206
     *
207
     * @return self
208
     */
209
    public function setMiddleware(Middleware $middleware) : self
210
    {
211
        $this->middleware = $middleware;
212
213
        return $this;
214
    }
215
216
    /**
217
     * Get middleware.
218
     *
219
     * @return Middleware
220
     */
221
    public function middleware() : Middleware
222
    {
223
        return $this->middleware;
224
    }
225
226
    /**
227
     * Set max upload size.
228
     *
229
     * @param int $uploadSize
230
     *
231
     * @return Server
232
     */
233
    public function setMaxUploadSize(int $uploadSize) : self
234
    {
235
        $this->maxUploadSize = $uploadSize;
236
237
        return $this;
238
    }
239
240
    /**
241
     * Get max upload size.
242
     *
243
     * @return int
244
     */
245
    public function getMaxUploadSize() : int
246
    {
247
        return $this->maxUploadSize;
248
    }
249
250
    /**
251
     * Handle all HTTP request.
252
     *
253
     * @return HttpResponse|BinaryFileResponse
254
     */
255
    public function serve()
256
    {
257
        $this->applyMiddleware();
258
259
        $requestMethod = $this->getRequest()->method();
260
261
        if ( ! in_array($requestMethod, $this->getRequest()->allowedHttpVerbs())) {
262
            return $this->response->send(null, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
263
        }
264
265
        $clientVersion = $this->getRequest()->header('Tus-Resumable');
266
267
        if ($clientVersion && $clientVersion !== self::TUS_PROTOCOL_VERSION) {
268
            return $this->response->send(null, HttpResponse::HTTP_PRECONDITION_FAILED, [
269
                'Tus-Version' => self::TUS_PROTOCOL_VERSION,
270
            ]);
271
        }
272
273
        $method = 'handle' . ucfirst(strtolower($requestMethod));
274
275
        return $this->{$method}();
276
    }
277
278
    /**
279
     * Apply middleware.
280
     *
281
     * @return void
282
     */
283
    protected function applyMiddleware()
284
    {
285
        $middleware = $this->middleware()->list();
286
287
        foreach ($middleware as $m) {
288
            $m->handle($this->getRequest(), $this->getResponse());
289
        }
290
    }
291
292
    /**
293
     * Handle OPTIONS request.
294
     *
295
     * @return HttpResponse
296
     */
297
    protected function handleOptions() : HttpResponse
298
    {
299
        $headers = [
300
            'Allow' => implode(',', $this->request->allowedHttpVerbs()),
301
            'Tus-Version' => self::TUS_PROTOCOL_VERSION,
302
            'Tus-Extension' => implode(',', self::TUS_EXTENSIONS),
303
            'Tus-Checksum-Algorithm' => $this->getSupportedHashAlgorithms(),
304
        ];
305
306
        $maxUploadSize = $this->getMaxUploadSize();
307
308
        if ($maxUploadSize > 0) {
309
            $headers['Tus-Max-Size'] = $maxUploadSize;
310
        }
311
312
        return $this->response->send(null, HttpResponse::HTTP_OK, $headers);
313
    }
314
315
    /**
316
     * Handle HEAD request.
317
     *
318
     * @return HttpResponse
319
     */
320
    protected function handleHead() : HttpResponse
321
    {
322
        $key = $this->request->key();
323
324
        if ( ! $fileMeta = $this->cache->get($key)) {
325
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
326
        }
327
328
        $offset = $fileMeta['offset'] ?? false;
329
330
        if (false === $offset) {
331
            return $this->response->send(null, HttpResponse::HTTP_GONE);
332
        }
333
334
        return $this->response->send(null, HttpResponse::HTTP_OK, $this->getHeadersForHeadRequest($fileMeta));
335
    }
336
337
    /**
338
     * Handle POST request.
339
     *
340
     * @return HttpResponse
341
     */
342
    protected function handlePost() : HttpResponse
343
    {
344
        $fileName   = $this->getRequest()->extractFileName();
345
        $uploadType = self::UPLOAD_TYPE_NORMAL;
346
347
        if (empty($fileName)) {
348
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
349
        }
350
351
        if ( ! $this->verifyUploadSize()) {
352
            return $this->response->send(null, HttpResponse::HTTP_REQUEST_ENTITY_TOO_LARGE);
353
        }
354
355
        $uploadKey = $this->getUploadKey();
356
        $filePath  = $this->uploadDir . DIRECTORY_SEPARATOR . $fileName;
357
358
        if ($this->getRequest()->isFinal()) {
359
            return $this->handleConcatenation($fileName, $filePath);
360
        }
361
362
        if ($this->getRequest()->isPartial()) {
363
            $filePath   = $this->getPathForPartialUpload($uploadKey) . $fileName;
364
            $uploadType = self::UPLOAD_TYPE_PARTIAL;
365
        }
366
367
        $checksum = $this->getClientChecksum();
368
        $location = $this->getRequest()->url() . $this->getApiPath() . '/' . $uploadKey;
369
370
        $file = $this->buildFile([
371
            'name' => $fileName,
372
            'offset' => 0,
373
            'size' => $this->getRequest()->header('Upload-Length'),
374
            'file_path' => $filePath,
375
            'location' => $location,
376
        ])->setChecksum($checksum);
377
378
        $this->cache->set($uploadKey, $file->details() + ['upload_type' => $uploadType]);
379
380
        $this->event()->dispatch(
381
            UploadCreated::NAME,
382
            new UploadCreated($file, $this->getRequest(), $this->getResponse())
383
        );
384
385
        return $this->response->send(
386
            null,
387
            HttpResponse::HTTP_CREATED,
388
            [
389
                'Location' => $location,
390
                'Upload-Expires' => $this->cache->get($uploadKey)['expires_at'],
391
            ]
392
        );
393
    }
394
395
    /**
396
     * Handle file concatenation.
397
     *
398
     * @param string $fileName
399
     * @param string $filePath
400
     *
401
     * @return HttpResponse
402
     */
403
    protected function handleConcatenation(string $fileName, string $filePath) : HttpResponse
404
    {
405
        $partials  = $this->getRequest()->extractPartials();
406
        $uploadKey = $this->getUploadKey();
407
        $files     = $this->getPartialsMeta($partials);
408
        $filePaths = array_column($files, 'file_path');
409
        $location  = $this->getRequest()->url() . $this->getApiPath() . '/' . $uploadKey;
410
411
        $file = $this->buildFile([
412
            'name' => $fileName,
413
            'offset' => 0,
414
            'size' => 0,
415
            'file_path' => $filePath,
416
            'location' => $location,
417
        ])->setFilePath($filePath);
418
419
        $file->setOffset($file->merge($files));
420
421
        // Verify checksum.
422
        $checksum = $this->getServerChecksum($filePath);
423
424
        if ($checksum !== $this->getClientChecksum()) {
425
            return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
426
        }
427
428
        $this->cache->set($uploadKey, $file->details() + ['upload_type' => self::UPLOAD_TYPE_FINAL]);
429
430
        // Cleanup.
431
        if ($file->delete($filePaths, true)) {
432
            $this->cache->deleteAll($partials);
433
        }
434
435
        $this->event()->dispatch(
436
            UploadMerged::NAME,
437
            new UploadMerged($file, $this->getRequest(), $this->getResponse())
438
        );
439
440
        return $this->response->send(
441
            ['data' => ['checksum' => $checksum]],
442
            HttpResponse::HTTP_CREATED,
443
            [
444
                'Location' => $location,
445
            ]
446
        );
447
    }
448
449
    /**
450
     * Handle PATCH request.
451
     *
452
     * @return HttpResponse
453
     */
454
    protected function handlePatch() : HttpResponse
455
    {
456
        $uploadKey = $this->request->key();
457
458
        if ( ! $meta = $this->cache->get($uploadKey)) {
459
            return $this->response->send(null, HttpResponse::HTTP_GONE);
460
        }
461
462
        $status = $this->verifyPatchRequest($meta);
463
464
        if (HttpResponse::HTTP_OK !== $status) {
465
            return $this->response->send(null, $status);
466
        }
467
468
        $file     = $this->buildFile($meta);
469
        $checksum = $meta['checksum'];
470
471
        try {
472
            $fileSize = $file->getFileSize();
473
            $offset   = $file->setKey($uploadKey)->setChecksum($checksum)->upload($fileSize);
474
475
            // If upload is done, verify checksum.
476
            if ($offset === $fileSize) {
477
                if ( ! $this->verifyChecksum($checksum, $meta['file_path'])) {
478
                    return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
479
                }
480
481
                $this->event()->dispatch(
482
                    UploadComplete::NAME,
483
                    new UploadComplete($file, $this->getRequest(), $this->getResponse())
484
                );
485
            } else {
486
                $this->event()->dispatch(
487
                    UploadProgress::NAME,
488
                    new UploadProgress($file, $this->getRequest(), $this->getResponse())
489
                );
490
            }
491
        } catch (FileException $e) {
492
            return $this->response->send($e->getMessage(), HttpResponse::HTTP_UNPROCESSABLE_ENTITY);
493
        } catch (OutOfRangeException $e) {
494
            return $this->response->send(null, HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
495
        } catch (ConnectionException $e) {
496
            return $this->response->send(null, HttpResponse::HTTP_CONTINUE);
497
        }
498
499
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
500
            'Content-Type' => self::HEADER_CONTENT_TYPE,
501
            'Upload-Expires' => $this->cache->get($uploadKey)['expires_at'],
502
            'Upload-Offset' => $offset,
503
        ]);
504
    }
505
506
    /**
507
     * Verify PATCH request.
508
     *
509
     * @param array $meta
510
     *
511
     * @return int
512
     */
513
    protected function verifyPatchRequest(array $meta) : int
514
    {
515
        if (self::UPLOAD_TYPE_FINAL === $meta['upload_type']) {
516
            return HttpResponse::HTTP_FORBIDDEN;
517
        }
518
519
        $uploadOffset = $this->request->header('upload-offset');
520
521
        if ($uploadOffset && $uploadOffset !== (string) $meta['offset']) {
522
            return HttpResponse::HTTP_CONFLICT;
523
        }
524
525
        $contentType = $this->request->header('Content-Type');
526
527
        if ($contentType !== self::HEADER_CONTENT_TYPE) {
528
            return HTTPRESPONSE::HTTP_UNSUPPORTED_MEDIA_TYPE;
529
        }
530
531
        return HttpResponse::HTTP_OK;
532
    }
533
534
    /**
535
     * Handle GET request.
536
     *
537
     * As per RFC7231, we need to treat HEAD and GET as an identical request.
538
     * All major PHP frameworks follows the same and silently transforms each
539
     * HEAD requests to GET.
540
     *
541
     * @return BinaryFileResponse|HttpResponse
542
     */
543
    protected function handleGet()
544
    {
545
        // We will treat '/files/<key>/get' as a download request.
546
        if ('get' === $this->request->key()) {
547
            return $this->handleDownload();
548
        }
549
550
        return $this->handleHead();
551
    }
552
553
    /**
554
     * Handle Download request.
555
     *
556
     * @return BinaryFileResponse|HttpResponse
557
     */
558
    protected function handleDownload()
559
    {
560
        $path = explode('/', str_replace('/get', '', $this->request->path()));
561
        $key  = end($path);
562
563
        if ( ! $fileMeta = $this->cache->get($key)) {
564
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
565
        }
566
567
        $resource = $fileMeta['file_path'] ?? null;
568
        $fileName = $fileMeta['name'] ?? null;
569
570
        if ( ! $resource || ! file_exists($resource)) {
571
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
572
        }
573
574
        return $this->response->download($resource, $fileName);
575
    }
576
577
    /**
578
     * Handle DELETE request.
579
     *
580
     * @return HttpResponse
581
     */
582
    protected function handleDelete() : HttpResponse
583
    {
584
        $key      = $this->request->key();
585
        $fileMeta = $this->cache->get($key);
586
        $resource = $fileMeta['file_path'] ?? null;
587
588
        if ( ! $resource) {
589
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
590
        }
591
592
        $isDeleted = $this->cache->delete($key);
593
594
        if ( ! $isDeleted || ! file_exists($resource)) {
595
            return $this->response->send(null, HttpResponse::HTTP_GONE);
596
        }
597
598
        unlink($resource);
599
600
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
601
            'Tus-Extension' => self::TUS_EXTENSION_TERMINATION,
602
        ]);
603
    }
604
605
    /**
606
     * Get required headers for head request.
607
     *
608
     * @param array $fileMeta
609
     *
610
     * @return array
611
     */
612
    protected function getHeadersForHeadRequest(array $fileMeta) : array
613
    {
614
        $headers = [
615
            'Upload-Length' => (int) $fileMeta['size'],
616
            'Upload-Offset' => (int) $fileMeta['offset'],
617
            'Cache-Control' => 'no-store',
618
        ];
619
620
        if (self::UPLOAD_TYPE_FINAL === $fileMeta['upload_type'] && $fileMeta['size'] !== $fileMeta['offset']) {
621
            unset($headers['Upload-Offset']);
622
        }
623
624
        if (self::UPLOAD_TYPE_NORMAL !== $fileMeta['upload_type']) {
625
            $headers += ['Upload-Concat' => $fileMeta['upload_type']];
626
        }
627
628
        return $headers;
629
    }
630
631
    /**
632
     * Build file object.
633
     *
634
     * @param array $meta
635
     *
636
     * @return File
637
     */
638
    protected function buildFile(array $meta) : File
639
    {
640
        $file = new File($meta['name'], $this->cache);
641
642
        if (array_key_exists('offset', $meta)) {
643
            $file->setMeta($meta['offset'], $meta['size'], $meta['file_path'], $meta['location']);
644
        }
645
646
        return $file;
647
    }
648
649
    /**
650
     * Get list of supported hash algorithms.
651
     *
652
     * @return string
653
     */
654
    protected function getSupportedHashAlgorithms() : string
655
    {
656
        $supportedAlgorithms = hash_algos();
657
658
        $algorithms = [];
659
        foreach ($supportedAlgorithms as $hashAlgo) {
660
            if (false !== strpos($hashAlgo, ',')) {
661
                $algorithms[] = "'{$hashAlgo}'";
662
            } else {
663
                $algorithms[] = $hashAlgo;
664
            }
665
        }
666
667
        return implode(',', $algorithms);
668
    }
669
670
    /**
671
     * Verify and get upload checksum from header.
672
     *
673
     * @return string|HttpResponse
674
     */
675
    protected function getClientChecksum()
676
    {
677
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
678
679
        if (empty($checksumHeader)) {
680
            return '';
681
        }
682
683
        list($checksumAlgorithm, $checksum) = explode(' ', $checksumHeader);
684
685
        $checksum = base64_decode($checksum);
686
687
        if ( ! in_array($checksumAlgorithm, hash_algos()) || false === $checksum) {
688
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
689
        }
690
691
        return $checksum;
692
    }
693
694
    /**
695
     * Get expired but incomplete uploads.
696
     *
697
     * @param array|null $contents
698
     *
699
     * @return bool
700
     */
701
    protected function isExpired($contents) : bool
702
    {
703
        $isExpired = empty($contents['expires_at']) || Carbon::parse($contents['expires_at'])->lt(Carbon::now());
704
705
        if ($isExpired && $contents['offset'] !== $contents['size']) {
706
            return true;
707
        }
708
709
        return false;
710
    }
711
712
    /**
713
     * Get path for partial upload.
714
     *
715
     * @param string $key
716
     *
717
     * @return string
718
     */
719
    protected function getPathForPartialUpload(string $key) : string
720
    {
721
        list($actualKey, /* $partialUploadKey */) = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key);
722
723
        $path = $this->uploadDir . DIRECTORY_SEPARATOR . $actualKey . DIRECTORY_SEPARATOR;
724
725
        if ( ! file_exists($path)) {
726
            mkdir($path);
727
        }
728
729
        return $path;
730
    }
731
732
    /**
733
     * Get metadata of partials.
734
     *
735
     * @param array $partials
736
     *
737
     * @return array
738
     */
739
    protected function getPartialsMeta(array $partials) : array
740
    {
741
        $files = [];
742
743
        foreach ($partials as $partial) {
744
            $fileMeta = $this->getCache()->get($partial);
745
746
            $files[] = $fileMeta;
747
        }
748
749
        return $files;
750
    }
751
752
    /**
753
     * Delete expired resources.
754
     *
755
     * @return array
756
     */
757
    public function handleExpiration() : array
758
    {
759
        $deleted   = [];
760
        $cacheKeys = $this->cache->keys();
761
762
        foreach ($cacheKeys as $key) {
763
            $fileMeta = $this->cache->get($key, true);
764
765
            if ( ! $this->isExpired($fileMeta)) {
766
                continue;
767
            }
768
769
            if ( ! $this->cache->delete($key)) {
770
                continue;
771
            }
772
773
            if (is_writable($fileMeta['file_path'])) {
774
                unlink($fileMeta['file_path']);
775
            }
776
777
            $deleted[] = $fileMeta;
778
        }
779
780
        return $deleted;
781
    }
782
783
    /**
784
     * Verify max upload size.
785
     *
786
     * @return bool
787
     */
788
    protected function verifyUploadSize() : bool
789
    {
790
        $maxUploadSize = $this->getMaxUploadSize();
791
792
        if ($maxUploadSize > 0 && $this->getRequest()->header('Upload-Length') > $maxUploadSize) {
793
            return false;
794
        }
795
796
        return true;
797
    }
798
799
    /**
800
     * Verify checksum if available.
801
     *
802
     * @param string $checksum
803
     * @param string $filePath
804
     *
805
     * @return bool
806
     */
807
    protected function verifyChecksum(string $checksum, string $filePath) : bool
808
    {
809
        // Skip if checksum is empty.
810
        if (empty($checksum)) {
811
            return true;
812
        }
813
814
        return $checksum === $this->getServerChecksum($filePath);
815
    }
816
817
    /**
818
     * No other methods are allowed.
819
     *
820
     * @param string $method
821
     * @param array  $params
822
     *
823
     * @return HttpResponse
824
     */
825
    public function __call(string $method, array $params)
826
    {
827
        return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
828
    }
829
}
830