Passed
Push — master ( 65e541...8a4e29 )
by Ankit
02:14
created

src/Tus/Server.php (6 issues)

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
    public const TUS_EXTENSION_CREATION = 'creation';
26
27
    /** @const string Tus Termination Extension */
28
    public const TUS_EXTENSION_TERMINATION = 'termination';
29
30
    /** @const string Tus Checksum Extension */
31
    public const TUS_EXTENSION_CHECKSUM = 'checksum';
32
33
    /** @const string Tus Expiration Extension */
34
    public const TUS_EXTENSION_EXPIRATION = 'expiration';
35
36
    /** @const string Tus Concatenation Extension */
37
    public const TUS_EXTENSION_CONCATENATION = 'concatenation';
38
39
    /** @const array All supported tus extensions */
40
    public 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
    private const HTTP_CHECKSUM_MISMATCH = 460;
50
51
    /** @const string Default checksum algorithm */
52
    private 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 3
    public function __construct($cacheAdapter = 'file')
83
    {
84 3
        $this->request    = new Request;
85 3
        $this->response   = new Response;
86 3
        $this->middleware = new Middleware;
87 3
        $this->uploadDir  = \dirname(__DIR__, 2) . '/' . 'uploads';
88
89 3
        $this->setCache($cacheAdapter);
90 3
    }
91
92
    /**
93
     * Set upload dir.
94
     *
95
     * @param string $path
96
     *
97
     * @return Server
98
     */
99 2
    public function setUploadDir(string $path) : self
100
    {
101 2
        $this->uploadDir = $path;
102
103 2
        return $this;
104
    }
105
106
    /**
107
     * Get upload dir.
108
     *
109
     * @return string
110
     */
111 1
    public function getUploadDir() : string
112
    {
113 1
        return $this->uploadDir;
114
    }
115
116
    /**
117
     * Get request.
118
     *
119
     * @return Request
120
     */
121 1
    public function getRequest() : Request
122
    {
123 1
        return $this->request;
124
    }
125
126
    /**
127
     * Get request.
128
     *
129
     * @return Response
130
     */
131 1
    public function getResponse() : Response
132
    {
133 1
        return $this->response;
134
    }
135
136
    /**
137
     * Get file checksum.
138
     *
139
     * @param string $filePath
140
     *
141
     * @return string
142
     */
143 1
    public function getServerChecksum(string $filePath) : string
144
    {
145 1
        return hash_file($this->getChecksumAlgorithm(), $filePath);
146
    }
147
148
    /**
149
     * Get checksum algorithm.
150
     *
151
     * @return string|null
152
     */
153 1
    public function getChecksumAlgorithm() : ?string
154
    {
155 1
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
156
157 1
        if (empty($checksumHeader)) {
158 1
            return self::DEFAULT_CHECKSUM_ALGORITHM;
159
        }
160
161 1
        [$checksumAlgorithm, /* $checksum */] = explode(' ', $checksumHeader);
162
163 1
        return $checksumAlgorithm;
164
    }
165
166
    /**
167
     * Set upload key.
168
     *
169
     * @param string $key
170
     *
171
     * @return Server
172
     */
173 1
    public function setUploadKey(string $key) : self
174
    {
175 1
        $this->uploadKey = $key;
176
177 1
        return $this;
178
    }
179
180
    /**
181
     * Get upload key from header.
182
     *
183
     * @return string|HttpResponse
184
     */
185 4
    public function getUploadKey()
186
    {
187 4
        if ( ! empty($this->uploadKey)) {
188 1
            return $this->uploadKey;
189
        }
190
191 3
        $key = $this->getRequest()->header('Upload-Key') ?? Uuid::uuid4()->toString();
192
193 3
        if (empty($key)) {
194 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
195
        }
196
197 2
        $this->uploadKey = $key;
198
199 2
        return $this->uploadKey;
200
    }
201
202
    /**
203
     * Set middleware.
204
     *
205
     * @param Middleware $middleware
206
     *
207
     * @return self
208
     */
209 1
    public function setMiddleware(Middleware $middleware) : self
210
    {
211 1
        $this->middleware = $middleware;
212
213 1
        return $this;
214
    }
215
216
    /**
217
     * Get middleware.
218
     *
219
     * @return Middleware
220
     */
221 1
    public function middleware() : Middleware
222
    {
223 1
        return $this->middleware;
224
    }
225
226
    /**
227
     * Set max upload size.
228
     *
229
     * @param int $uploadSize
230
     *
231
     * @return Server
232
     */
233 2
    public function setMaxUploadSize(int $uploadSize) : self
234
    {
235 2
        $this->maxUploadSize = $uploadSize;
236
237 2
        return $this;
238
    }
239
240
    /**
241
     * Get max upload size.
242
     *
243
     * @return int
244
     */
245 1
    public function getMaxUploadSize() : int
246
    {
247 1
        return $this->maxUploadSize;
248
    }
249
250
    /**
251
     * Handle all HTTP request.
252
     *
253
     * @return HttpResponse|BinaryFileResponse
254
     */
255 5
    public function serve()
256
    {
257 5
        $this->applyMiddleware();
258
259 5
        $requestMethod = $this->getRequest()->method();
260
261 5
        if ( ! \in_array($requestMethod, $this->getRequest()->allowedHttpVerbs(), true)) {
262 1
            return $this->response->send(null, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
263
        }
264
265 4
        $clientVersion = $this->getRequest()->header('Tus-Resumable');
266
267 4
        if ($clientVersion && $clientVersion !== self::TUS_PROTOCOL_VERSION) {
268 1
            return $this->response->send(null, HttpResponse::HTTP_PRECONDITION_FAILED, [
269 1
                'Tus-Version' => self::TUS_PROTOCOL_VERSION,
270
            ]);
271
        }
272
273 3
        $method = 'handle' . ucfirst(strtolower($requestMethod));
274
275 3
        return $this->{$method}();
276
    }
277
278
    /**
279
     * Apply middleware.
280
     *
281
     * @return void
282
     */
283 1
    protected function applyMiddleware()
284
    {
285 1
        $middleware = $this->middleware()->list();
286
287 1
        foreach ($middleware as $m) {
288 1
            $m->handle($this->getRequest(), $this->getResponse());
289
        }
290 1
    }
291
292
    /**
293
     * Handle OPTIONS request.
294
     *
295
     * @return HttpResponse
296
     */
297 2
    protected function handleOptions() : HttpResponse
298
    {
299
        $headers = [
300 2
            'Allow' => implode(',', $this->request->allowedHttpVerbs()),
301 2
            'Tus-Version' => self::TUS_PROTOCOL_VERSION,
302 2
            'Tus-Extension' => implode(',', self::TUS_EXTENSIONS),
303 2
            'Tus-Checksum-Algorithm' => $this->getSupportedHashAlgorithms(),
304
        ];
305
306 2
        $maxUploadSize = $this->getMaxUploadSize();
307
308 2
        if ($maxUploadSize > 0) {
309 1
            $headers['Tus-Max-Size'] = $maxUploadSize;
310
        }
311
312 2
        return $this->response->send(null, HttpResponse::HTTP_OK, $headers);
313
    }
314
315
    /**
316
     * Handle HEAD request.
317
     *
318
     * @return HttpResponse
319
     */
320 5
    protected function handleHead() : HttpResponse
321
    {
322 5
        $key = $this->request->key();
323
324 5
        if ( ! $fileMeta = $this->cache->get($key)) {
325 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
326
        }
327
328 4
        $offset = $fileMeta['offset'] ?? false;
329
330 4
        if (false === $offset) {
331 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
332
        }
333
334 3
        return $this->response->send(null, HttpResponse::HTTP_OK, $this->getHeadersForHeadRequest($fileMeta));
335
    }
336
337
    /**
338
     * Handle POST request.
339
     *
340
     * @return HttpResponse
341
     */
342 5
    protected function handlePost() : HttpResponse
343
    {
344 5
        $fileName   = $this->getRequest()->extractFileName();
345 5
        $uploadType = self::UPLOAD_TYPE_NORMAL;
346
347 5
        if (empty($fileName)) {
348 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
349
        }
350
351 4
        if ( ! $this->verifyUploadSize()) {
352 1
            return $this->response->send(null, HttpResponse::HTTP_REQUEST_ENTITY_TOO_LARGE);
353
        }
354
355 3
        $uploadKey = $this->getUploadKey();
356 3
        $filePath  = $this->uploadDir . '/' . $fileName;
357
358 3
        if ($this->getRequest()->isFinal()) {
359 1
            return $this->handleConcatenation($fileName, $filePath);
360
        }
361
362 2
        if ($this->getRequest()->isPartial()) {
363 1
            $filePath   = $this->getPathForPartialUpload($uploadKey) . $fileName;
364 1
            $uploadType = self::UPLOAD_TYPE_PARTIAL;
365
        }
366
367 2
        $checksum = $this->getClientChecksum();
368 2
        $location = $this->getRequest()->url() . $this->getApiPath() . '/' . $uploadKey;
369
370 2
        $file = $this->buildFile([
371 2
            'name' => $fileName,
372 2
            'offset' => 0,
373 2
            'size' => $this->getRequest()->header('Upload-Length'),
374 2
            'file_path' => $filePath,
375 2
            'location' => $location,
376 2
        ])->setKey($uploadKey)->setChecksum($checksum)->setUploadMetadata($this->getRequest()->extractAllMeta());
377
378 2
        $this->cache->set($uploadKey, $file->details() + ['upload_type' => $uploadType]);
379
380
        $headers = [
381 2
            'Location' => $location,
382 2
            'Upload-Expires' => $this->cache->get($uploadKey)['expires_at'],
383
        ];
384
385 2
        $this->event()->dispatch(UploadCreated::NAME, new UploadCreated($file, $this->getRequest(), $this->getResponse()->setHeaders($headers)));
0 ignored issues
show
TusPhp\Events\UploadCreated::NAME of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

385
        $this->event()->dispatch(/** @scrutinizer ignore-type */ UploadCreated::NAME, new UploadCreated($file, $this->getRequest(), $this->getResponse()->setHeaders($headers)));
Loading history...
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new TusPhp\Events\Upload...->setHeaders($headers)). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

385
        $this->event()->/** @scrutinizer ignore-call */ dispatch(UploadCreated::NAME, new UploadCreated($file, $this->getRequest(), $this->getResponse()->setHeaders($headers)));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
386
387 2
        return $this->response->send(null, HttpResponse::HTTP_CREATED, $headers);
388
    }
389
390
    /**
391
     * Handle file concatenation.
392
     *
393
     * @param string $fileName
394
     * @param string $filePath
395
     *
396
     * @return HttpResponse
397
     */
398 2
    protected function handleConcatenation(string $fileName, string $filePath) : HttpResponse
399
    {
400 2
        $partials  = $this->getRequest()->extractPartials();
401 2
        $uploadKey = $this->getUploadKey();
402 2
        $files     = $this->getPartialsMeta($partials);
403 2
        $filePaths = array_column($files, 'file_path');
404 2
        $location  = $this->getRequest()->url() . $this->getApiPath() . '/' . $uploadKey;
405
406 2
        $file = $this->buildFile([
407 2
            'name' => $fileName,
408 2
            'offset' => 0,
409 2
            'size' => 0,
410 2
            'file_path' => $filePath,
411 2
            'location' => $location,
412 2
        ])->setFilePath($filePath)->setKey($uploadKey)->setUploadMetadata($this->getRequest()->extractAllMeta());
413
414 2
        $file->setOffset($file->merge($files));
415
416
        // Verify checksum.
417 2
        $checksum = $this->getServerChecksum($filePath);
418
419 2
        if ($checksum !== $this->getClientChecksum()) {
420 1
            return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
421
        }
422
423 1
        $file->setChecksum($checksum);
424 1
        $this->cache->set($uploadKey, $file->details() + ['upload_type' => self::UPLOAD_TYPE_FINAL]);
425
426
        // Cleanup.
427 1
        if ($file->delete($filePaths, true)) {
428 1
            $this->cache->deleteAll($partials);
429
        }
430
431 1
        $this->event()->dispatch(
432 1
            UploadMerged::NAME,
0 ignored issues
show
TusPhp\Events\UploadMerged::NAME of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

432
            /** @scrutinizer ignore-type */ UploadMerged::NAME,
Loading history...
433 1
            new UploadMerged($file, $this->getRequest(), $this->getResponse())
0 ignored issues
show
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new TusPhp\Events\Upload..., $this->getResponse()). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

433
        $this->event()->/** @scrutinizer ignore-call */ dispatch(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
434
        );
435
436 1
        return $this->response->send(
437 1
            ['data' => ['checksum' => $checksum]],
438 1
            HttpResponse::HTTP_CREATED,
439
            [
440 1
                'Location' => $location,
441
            ]
442
        );
443
    }
444
445
    /**
446
     * Handle PATCH request.
447
     *
448
     * @return HttpResponse
449
     */
450 10
    protected function handlePatch() : HttpResponse
451
    {
452 10
        $uploadKey = $this->request->key();
453
454 10
        if ( ! $meta = $this->cache->get($uploadKey)) {
455 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
456
        }
457
458 9
        $status = $this->verifyPatchRequest($meta);
459
460 9
        if (HttpResponse::HTTP_OK !== $status) {
461 3
            return $this->response->send(null, $status);
462
        }
463
464 6
        $file     = $this->buildFile($meta)->setUploadMetadata($meta['metadata'] ?? []);
465 6
        $checksum = $meta['checksum'];
466
467
        try {
468 6
            $fileSize = $file->getFileSize();
469 6
            $offset   = $file->setKey($uploadKey)->setChecksum($checksum)->upload($fileSize);
470
471
            // If upload is done, verify checksum.
472 3
            if ($offset === $fileSize) {
473 2
                if ( ! $this->verifyChecksum($checksum, $meta['file_path'])) {
474 1
                    return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
475
                }
476
477 1
                $this->event()->dispatch(
478 1
                    UploadComplete::NAME,
0 ignored issues
show
TusPhp\Events\UploadComplete::NAME of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

478
                    /** @scrutinizer ignore-type */ UploadComplete::NAME,
Loading history...
479 1
                    new UploadComplete($file, $this->getRequest(), $this->getResponse())
0 ignored issues
show
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new TusPhp\Events\Upload..., $this->getResponse()). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

479
                $this->event()->/** @scrutinizer ignore-call */ dispatch(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
480
                );
481
            } else {
482 1
                $this->event()->dispatch(
483 1
                    UploadProgress::NAME,
484 2
                    new UploadProgress($file, $this->getRequest(), $this->getResponse())
485
                );
486
            }
487 3
        } catch (FileException $e) {
488 1
            return $this->response->send($e->getMessage(), HttpResponse::HTTP_UNPROCESSABLE_ENTITY);
489 2
        } catch (OutOfRangeException $e) {
490 1
            return $this->response->send(null, HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
491 1
        } catch (ConnectionException $e) {
492 1
            return $this->response->send(null, HttpResponse::HTTP_CONTINUE);
493
        }
494
495 2
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
496 2
            'Content-Type' => self::HEADER_CONTENT_TYPE,
497 2
            'Upload-Expires' => $this->cache->get($uploadKey)['expires_at'],
498 2
            'Upload-Offset' => $offset,
499
        ]);
500
    }
501
502
    /**
503
     * Verify PATCH request.
504
     *
505
     * @param array $meta
506
     *
507
     * @return int
508
     */
509 9
    protected function verifyPatchRequest(array $meta) : int
510
    {
511 9
        if (self::UPLOAD_TYPE_FINAL === $meta['upload_type']) {
512 1
            return HttpResponse::HTTP_FORBIDDEN;
513
        }
514
515 8
        $uploadOffset = $this->request->header('upload-offset');
516
517 8
        if ($uploadOffset && $uploadOffset !== (string) $meta['offset']) {
518 1
            return HttpResponse::HTTP_CONFLICT;
519
        }
520
521 7
        $contentType = $this->request->header('Content-Type');
522
523 7
        if ($contentType !== self::HEADER_CONTENT_TYPE) {
524 1
            return HTTPRESPONSE::HTTP_UNSUPPORTED_MEDIA_TYPE;
525
        }
526
527 6
        return HttpResponse::HTTP_OK;
528
    }
529
530
    /**
531
     * Handle GET request.
532
     *
533
     * As per RFC7231, we need to treat HEAD and GET as an identical request.
534
     * All major PHP frameworks follows the same and silently transforms each
535
     * HEAD requests to GET.
536
     *
537
     * @return BinaryFileResponse|HttpResponse
538
     */
539 6
    protected function handleGet()
540
    {
541
        // We will treat '/files/<key>/get' as a download request.
542 6
        if ('get' === $this->request->key()) {
543 5
            return $this->handleDownload();
544
        }
545
546 1
        return $this->handleHead();
547
    }
548
549
    /**
550
     * Handle Download request.
551
     *
552
     * @return BinaryFileResponse|HttpResponse
553
     */
554 4
    protected function handleDownload()
555
    {
556 4
        $path = explode('/', str_replace('/get', '', $this->request->path()));
557 4
        $key  = end($path);
558
559 4
        if ( ! $fileMeta = $this->cache->get($key)) {
560 2
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
561
        }
562
563 2
        $resource = $fileMeta['file_path'] ?? null;
564 2
        $fileName = $fileMeta['name'] ?? null;
565
566 2
        if ( ! $resource || ! file_exists($resource)) {
567 1
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
568
        }
569
570 1
        return $this->response->download($resource, $fileName);
571
    }
572
573
    /**
574
     * Handle DELETE request.
575
     *
576
     * @return HttpResponse
577
     */
578 3
    protected function handleDelete() : HttpResponse
579
    {
580 3
        $key      = $this->request->key();
581 3
        $fileMeta = $this->cache->get($key);
582 3
        $resource = $fileMeta['file_path'] ?? null;
583
584 3
        if ( ! $resource) {
585 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
586
        }
587
588 2
        $isDeleted = $this->cache->delete($key);
589
590 2
        if ( ! $isDeleted || ! file_exists($resource)) {
591 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
592
        }
593
594 1
        unlink($resource);
595
596 1
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
597 1
            'Tus-Extension' => self::TUS_EXTENSION_TERMINATION,
598
        ]);
599
    }
600
601
    /**
602
     * Get required headers for head request.
603
     *
604
     * @param array $fileMeta
605
     *
606
     * @return array
607
     */
608 4
    protected function getHeadersForHeadRequest(array $fileMeta) : array
609
    {
610
        $headers = [
611 4
            'Upload-Length' => (int) $fileMeta['size'],
612 4
            'Upload-Offset' => (int) $fileMeta['offset'],
613 4
            'Cache-Control' => 'no-store',
614
        ];
615
616 4
        if (self::UPLOAD_TYPE_FINAL === $fileMeta['upload_type'] && $fileMeta['size'] !== $fileMeta['offset']) {
617 2
            unset($headers['Upload-Offset']);
618
        }
619
620 4
        if (self::UPLOAD_TYPE_NORMAL !== $fileMeta['upload_type']) {
621 3
            $headers += ['Upload-Concat' => $fileMeta['upload_type']];
622
        }
623
624 4
        return $headers;
625
    }
626
627
    /**
628
     * Build file object.
629
     *
630
     * @param array $meta
631
     *
632
     * @return File
633
     */
634 1
    protected function buildFile(array $meta) : File
635
    {
636 1
        $file = new File($meta['name'], $this->cache);
637
638 1
        if (\array_key_exists('offset', $meta)) {
639 1
            $file->setMeta($meta['offset'], $meta['size'], $meta['file_path'], $meta['location']);
640
        }
641
642 1
        return $file;
643
    }
644
645
    /**
646
     * Get list of supported hash algorithms.
647
     *
648
     * @return string
649
     */
650 1
    protected function getSupportedHashAlgorithms() : string
651
    {
652 1
        $supportedAlgorithms = hash_algos();
653
654 1
        $algorithms = [];
655 1
        foreach ($supportedAlgorithms as $hashAlgo) {
656 1
            if (false !== strpos($hashAlgo, ',')) {
657 1
                $algorithms[] = "'{$hashAlgo}'";
658
            } else {
659 1
                $algorithms[] = $hashAlgo;
660
            }
661
        }
662
663 1
        return implode(',', $algorithms);
664
    }
665
666
    /**
667
     * Verify and get upload checksum from header.
668
     *
669
     * @return string|HttpResponse
670
     */
671 4
    protected function getClientChecksum()
672
    {
673 4
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
674
675 4
        if (empty($checksumHeader)) {
676 1
            return '';
677
        }
678
679 3
        [$checksumAlgorithm, $checksum] = explode(' ', $checksumHeader);
680
681 3
        $checksum = base64_decode($checksum);
682
683 3
        if (false === $checksum || ! \in_array($checksumAlgorithm, hash_algos(), true)) {
684 2
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
685
        }
686
687 1
        return $checksum;
688
    }
689
690
    /**
691
     * Get expired but incomplete uploads.
692
     *
693
     * @param array|null $contents
694
     *
695
     * @return bool
696
     */
697 3
    protected function isExpired($contents) : bool
698
    {
699 3
        $isExpired = empty($contents['expires_at']) || Carbon::parse($contents['expires_at'])->lt(Carbon::now());
700
701 3
        if ($isExpired && $contents['offset'] !== $contents['size']) {
702 3
            return true;
703
        }
704
705 2
        return false;
706
    }
707
708
    /**
709
     * Get path for partial upload.
710
     *
711
     * @param string $key
712
     *
713
     * @return string
714
     */
715 1
    protected function getPathForPartialUpload(string $key) : string
716
    {
717 1
        [$actualKey, /* $partialUploadKey */] = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key);
718
719 1
        $path = $this->uploadDir . '/' . $actualKey . '/';
720
721 1
        if ( ! file_exists($path)) {
722 1
            mkdir($path);
723
        }
724
725 1
        return $path;
726
    }
727
728
    /**
729
     * Get metadata of partials.
730
     *
731
     * @param array $partials
732
     *
733
     * @return array
734
     */
735 3
    protected function getPartialsMeta(array $partials) : array
736
    {
737 3
        $files = [];
738
739 3
        foreach ($partials as $partial) {
740 3
            $fileMeta = $this->getCache()->get($partial);
741
742 3
            $files[] = $fileMeta;
743
        }
744
745 3
        return $files;
746
    }
747
748
    /**
749
     * Delete expired resources.
750
     *
751
     * @return array
752
     */
753 2
    public function handleExpiration() : array
754
    {
755 2
        $deleted   = [];
756 2
        $cacheKeys = $this->cache->keys();
757
758 2
        foreach ($cacheKeys as $key) {
759 2
            $fileMeta = $this->cache->get($key, true);
760
761 2
            if ( ! $this->isExpired($fileMeta)) {
762 1
                continue;
763
            }
764
765 2
            if ( ! $this->cache->delete($key)) {
766 1
                continue;
767
            }
768
769 1
            if (is_writable($fileMeta['file_path'])) {
770 1
                unlink($fileMeta['file_path']);
771
            }
772
773 1
            $deleted[] = $fileMeta;
774
        }
775
776 2
        return $deleted;
777
    }
778
779
    /**
780
     * Verify max upload size.
781
     *
782
     * @return bool
783
     */
784 1
    protected function verifyUploadSize() : bool
785
    {
786 1
        $maxUploadSize = $this->getMaxUploadSize();
787
788 1
        if ($maxUploadSize > 0 && $this->getRequest()->header('Upload-Length') > $maxUploadSize) {
789 1
            return false;
790
        }
791
792 1
        return true;
793
    }
794
795
    /**
796
     * Verify checksum if available.
797
     *
798
     * @param string $checksum
799
     * @param string $filePath
800
     *
801
     * @return bool
802
     */
803 1
    protected function verifyChecksum(string $checksum, string $filePath) : bool
804
    {
805
        // Skip if checksum is empty.
806 1
        if (empty($checksum)) {
807 1
            return true;
808
        }
809
810 1
        return $checksum === $this->getServerChecksum($filePath);
811
    }
812
813
    /**
814
     * No other methods are allowed.
815
     *
816
     * @param string $method
817
     * @param array  $params
818
     *
819
     * @return HttpResponse
820
     */
821 1
    public function __call(string $method, array $params)
822
    {
823 1
        return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
824
    }
825
}
826