Completed
Pull Request — master (#11)
by Ankit
02:36
created

Server::getSupportedHashAlgorithms()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 3
rs 9.4285
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 TusPhp\Cache\Cacheable;
10
use TusPhp\Exception\FileException;
11
use TusPhp\Exception\ConnectionException;
12
use TusPhp\Exception\OutOfRangeException;
13
use Illuminate\Http\Response as HttpResponse;
14
use Symfony\Component\HttpFoundation\BinaryFileResponse;
15
16
class Server extends AbstractTus
17
{
18
    /** @const Tus Creation Extension */
19
    const TUS_EXTENSION_CREATION = 'creation';
20
21
    /** @const Tus Termination Extension */
22
    const TUS_EXTENSION_TERMINATION = 'termination';
23
24
    /** @const Tus Checksum Extension */
25
    const TUS_EXTENSION_CHECKSUM = 'checksum';
26
27
    /** @const Tus Expiration Extension */
28
    const TUS_EXTENSION_EXPIRATION = 'expiration';
29
30
    /** @const Tus Concatenation Extension */
31
    const TUS_EXTENSION_CONCATENATION = 'concatenation';
32
33
    /** @const 460 Checksum Mismatch */
34
    const HTTP_CHECKSUM_MISMATCH = 460;
35
36
    /** @var Request */
37
    protected $request;
38
39
    /** @var Response */
40
    protected $response;
41
42
    /** @var string */
43
    protected $uploadDir;
44
45
    /**
46
     * TusServer constructor.
47
     *
48
     * @param Cacheable|string $cacheAdapter
49
     */
50 3
    public function __construct($cacheAdapter = 'file')
51
    {
52 3
        $this->request   = new Request;
53 3
        $this->response  = new Response;
54 3
        $this->uploadDir = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'uploads';
55
56 3
        $this->setCache($cacheAdapter);
57 3
    }
58
59
    /**
60
     * Set upload dir.
61
     *
62
     * @param string $path
63
     *
64
     * @return void
65
     */
66 1
    public function setUploadDir(string $path)
67
    {
68 1
        $this->uploadDir = $path;
69 1
    }
70
71
    /**
72
     * Get upload dir.
73
     *
74
     * @return string
75
     */
76 1
    public function getUploadDir() : string
77
    {
78 1
        return $this->uploadDir;
79
    }
80
81
    /**
82
     * Get request.
83
     *
84
     * @return Request
85
     */
86 1
    public function getRequest() : Request
87
    {
88 1
        return $this->request;
89
    }
90
91
    /**
92
     * Get request.
93
     *
94
     * @return Response
95
     */
96 1
    public function getResponse() : Response
97
    {
98 1
        return $this->response;
99
    }
100
101
    /**
102
     * Get file checksum.
103
     *
104
     * @param string $filePath
105
     *
106
     * @return string
107
     */
108
    public function getChecksum(string $filePath)
109
    {
110
        return hash_file($this->getChecksumAlgorithm(), $filePath);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getChecksumAlgorithm() targeting TusPhp\Tus\Server::getChecksumAlgorithm() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
111
    }
112
113
    /**
114
     * Get checksum algorithm.
115
     *
116
     * @return null
117
     */
118
    public function getChecksumAlgorithm()
119
    {
120
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
121
122
        if (empty($checksumHeader)) {
123
            return null;
124
        }
125
126
        list($checksumAlgorithm) = explode(' ', $checksumHeader);
127
128
        return $checksumAlgorithm;
129
    }
130
131
    /**
132
     * Handle all HTTP request.
133
     *
134
     * @return null|HttpResponse
135
     */
136 2
    public function serve()
137
    {
138 2
        $method = $this->getRequest()->method();
139
140 2
        if ( ! in_array($method, $this->request->allowedHttpVerbs())) {
141 1
            return $this->response->send(null, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
142
        }
143
144 1
        $method = 'handle' . ucfirst(strtolower($method));
145
146 1
        $this->{$method}();
147
148 1
        $this->exit();
149 1
    }
150
151
    /**
152
     * Exit from current php process.
153
     *
154
     * @codeCoverageIgnore
155
     */
156
    protected function exit()
157
    {
158
        exit(0);
159
    }
160
161
    /**
162
     * Handle OPTIONS request.
163
     *
164
     * @return HttpResponse
165
     */
166 1
    protected function handleOptions() : HttpResponse
167
    {
168 1
        return $this->response->send(
169 1
            null,
170 1
            HttpResponse::HTTP_OK,
171
            [
172 1
                'Allow' => $this->request->allowedHttpVerbs(),
173 1
                'Tus-Version' => self::TUS_PROTOCOL_VERSION,
174 1
                'Tus-Extension' => implode(',', [
175 1
                    self::TUS_EXTENSION_CREATION,
176 1
                    self::TUS_EXTENSION_TERMINATION,
177 1
                    self::TUS_EXTENSION_CHECKSUM,
178 1
                    self::TUS_EXTENSION_EXPIRATION,
179 1
                    self::TUS_EXTENSION_CONCATENATION,
180
                ]),
181 1
                'Tus-Checksum-Algorithm' => $this->getSupportedHashAlgorithms(),
182
            ]
183
        );
184
    }
185
186
    /**
187
     * Handle HEAD request.
188
     *
189
     * @return HttpResponse
190
     */
191 3
    protected function handleHead() : HttpResponse
192
    {
193 3
        $checksum = $this->request->checksum();
194
195 3
        if ( ! $this->cache->get($checksum)) {
196 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
197
        }
198
199 2
        $offset = $this->cache->get($checksum)['offset'] ?? false;
200
201 2
        if (false === $offset) {
202 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
203
        }
204
205 1
        return $this->response->send(null, HttpResponse::HTTP_OK, [
206 1
            'Upload-Offset' => (int) $offset,
207 1
            'Cache-Control' => 'no-store',
208 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
209
        ]);
210
    }
211
212
    /**
213
     * Handle POST request.
214
     *
215
     * @return HttpResponse
216
     */
217 2
    protected function handlePost() : HttpResponse
218
    {
219 2
        $fileName = $this->getRequest()->extractFileName();
220
221 2
        if (empty($fileName)) {
222 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
223
        }
224
225 1
        $checksum = $this->getUploadChecksum();
226 1
        $filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $fileName;
227
228 1
        if ($this->getRequest()->isFinal()) {
229
            return $this->handleConcatenation($fileName, $filePath);
230
        }
231
232 1
        if ($this->getRequest()->isPartial()) {
233
            $filePath = $this->getPathForPartialUpload($checksum) . $fileName;
234
        }
235
236 1
        $location = $this->getRequest()->url() . '/' . basename($this->uploadDir) . '/' . $fileName;
237
238 1
        $file = $this->buildFile([
239 1
            'name' => $fileName,
240 1
            'offset' => 0,
241 1
            'size' => $this->getRequest()->header('Upload-Length'),
242 1
            'file_path' => $filePath,
243 1
            'location' => $location,
244 1
        ])->setChecksum($checksum);
245
246 1
        $this->cache->set($checksum, $file->details());
247
248 1
        return $this->response->send(
249 1
            ['data' => ['checksum' => $checksum]],
250 1
            HttpResponse::HTTP_CREATED,
251
            [
252 1
                'Location' => $location,
253 1
                'Upload-Expires' => $this->cache->get($checksum)['expires_at'],
254 1
                'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
255
            ]
256
        );
257
    }
258
259
    /**
260
     * Handle file concatenation.
261
     *
262
     * @param string $fileName
263
     * @param string $filePath
264
     *
265
     * @return HttpResponse
266
     */
267
    protected function handleConcatenation(string $fileName, string $filePath) : HttpResponse
268
    {
269
        $files    = [];
270
        $partials = $this->getRequest()->extractPartials();
271
        $location = $this->getRequest()->url() . '/' . basename($this->uploadDir) . '/' . $fileName;
272
273
        foreach ($partials as $partial) {
274
            $fileMeta = $this->getCache()->get($partial);
275
276
            $files[] = $fileMeta['file_path'];
277
        }
278
279
        $file = (new File($fileName, $this->cache))
280
            ->setFilePath($filePath);
281
282
        $file->merge($files);
283
284
        // Verify checksum.
285
        $checksum = $this->getChecksum($filePath);
286
287
        if ($checksum !== $this->getUploadChecksum()) {
288
            return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
289
        }
290
291
        // Cleanup.
292
        $file->delete($files, true);
293
294
        return $this->response->send(
295
            ['data' => ['checksum' => $checksum]],
296
            HttpResponse::HTTP_CREATED,
297
            [
298
                'Location' => $location,
299
                'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
300
            ]
301
        );
302
    }
303
304
    /**
305
     * Handle PATCH request.
306
     *
307
     * @return HttpResponse
308
     */
309 6
    protected function handlePatch() : HttpResponse
310
    {
311 6
        $checksum = $this->request->checksum();
312
313 6
        if ( ! $this->cache->get($checksum)) {
314 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
315
        }
316
317 5
        $meta = $this->cache->get($checksum);
318 5
        $file = $this->buildFile($meta);
319
320
        try {
321 5
            $fileSize = $file->getFileSize();
322 5
            $offset   = $file->setChecksum($checksum)->upload($fileSize);
323
324
            // If upload is done, verify checksum.
325 2
            if ($offset === $fileSize && $checksum !== $this->getUploadChecksum()) {
326 2
                return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
327
            }
328 3
        } catch (FileException $e) {
329 1
            return $this->response->send($e->getMessage(), HttpResponse::HTTP_UNPROCESSABLE_ENTITY);
330 2
        } catch (OutOfRangeException $e) {
331 1
            return $this->response->send(null, HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
332 1
        } catch (ConnectionException $e) {
333 1
            return $this->response->send(null, HttpResponse::HTTP_CONTINUE);
334
        }
335
336 1
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
337 1
            'Upload-Expires' => $this->cache->get($checksum)['expires_at'],
338 1
            'Upload-Offset' => $offset,
339 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
340
        ]);
341
    }
342
343
    /**
344
     * Handle GET request.
345
     *
346
     * @return BinaryFileResponse|HttpResponse
347
     */
348 4
    protected function handleGet()
349
    {
350 4
        $checksum = $this->request->checksum();
351
352 4
        if (empty($checksum)) {
353 1
            return $this->response->send('400 bad request.', HttpResponse::HTTP_BAD_REQUEST);
354
        }
355
356 3
        $fileMeta = $this->cache->get($checksum);
357
358 3
        if ( ! $fileMeta) {
359 1
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
360
        }
361
362 2
        $resource = $fileMeta['file_path'] ?? null;
363 2
        $fileName = $fileMeta['name'] ?? null;
364
365 2
        if ( ! $resource || ! file_exists($resource)) {
366 1
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
367
        }
368
369 1
        return $this->response->download($resource, $fileName);
370
    }
371
372
    /**
373
     * Handle DELETE request.
374
     *
375
     * @return HttpResponse
376
     */
377 3
    protected function handleDelete() : HttpResponse
378
    {
379 3
        $checksum = $this->request->checksum();
380 3
        $fileMeta = $this->cache->get($checksum);
381 3
        $resource = $fileMeta['file_path'] ?? null;
382
383 3
        if ( ! $resource) {
384 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
385
        }
386
387 2
        $isDeleted = $this->cache->delete($checksum);
388
389 2
        if ( ! $isDeleted || ! file_exists($resource)) {
390 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
391
        }
392
393 1
        unlink($resource);
394
395 1
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
396 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
397 1
            'Tus-Extension' => self::TUS_EXTENSION_TERMINATION,
398
        ]);
399
    }
400
401
    /**
402
     * Build file object.
403
     *
404
     * @param array $meta
405
     *
406
     * @return File
407
     */
408 1
    protected function buildFile(array $meta) : File
409
    {
410 1
        return (new File($meta['name'], $this->cache))
411 1
            ->setMeta($meta['offset'], $meta['size'], $meta['file_path'], $meta['location']);
412
    }
413
414
    /**
415
     * Get list of supported hash algorithms.
416
     *
417
     * @return string
418
     */
419 1
    protected function getSupportedHashAlgorithms()
420
    {
421 1
        $supportedAlgorithms = hash_algos();
422
423 1
        $algorithms = [];
424 1
        foreach ($supportedAlgorithms as $hashAlgo) {
425 1
            if (false !== strpos($hashAlgo, ',')) {
426 1
                $algorithms[] = "'{$hashAlgo}'";
427
            } else {
428 1
                $algorithms[] = $hashAlgo;
429
            }
430
        }
431
432 1
        return implode(',', $algorithms);
433
    }
434
435
    /**
436
     * Verify and get upload checksum from header.
437
     *
438
     * @return string|HttpResponse
439
     */
440 4
    protected function getUploadChecksum()
441
    {
442 4
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
443
444 4
        if (empty($checksumHeader)) {
445 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
446
        }
447
448 3
        list($checksumAlgorithm, $checksum) = explode(' ', $checksumHeader);
449
450 3
        $checksum = base64_decode($checksum);
451
452 3
        if ( ! in_array($checksumAlgorithm, hash_algos()) || false === $checksum) {
453 2
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
454
        }
455
456 1
        return $checksum;
457
    }
458
459
    /**
460
     * Get expired but incomplete uploads.
461
     *
462
     * @param array|null $contents
463
     *
464
     * @return bool
465
     */
466 3
    protected function isExpired($contents) : bool
467
    {
468 3
        $isExpired = empty($contents['expires_at']) || Carbon::parse($contents['expires_at'])->lt(Carbon::now());
469
470 3
        if ($isExpired && $contents['offset'] !== $contents['size']) {
471 3
            return true;
472
        }
473
474 2
        return false;
475
    }
476
477
    /**
478
     * Get path for partial upload.
479
     *
480
     * @param string $checksum
481
     *
482
     * @return string
483
     */
484
    protected function getPathForPartialUpload(string $checksum) : string
485
    {
486
        list($actualChecksum) = explode(self::PARTIAL_UPLOAD_NAME_SEPARATOR, $checksum);
487
488
        $path = $this->uploadDir . DIRECTORY_SEPARATOR . $actualChecksum . DIRECTORY_SEPARATOR;
489
490
        if ( ! file_exists($path)) {
491
            mkdir($path);
492
        }
493
494
        return $path;
495
    }
496
497
    /**
498
     * Delete expired resources.
499
     *
500
     * @return array
501
     */
502 2
    public function handleExpiration()
503
    {
504 2
        $deleted   = [];
505 2
        $cacheKeys = $this->cache->keys();
506
507 2
        foreach ($cacheKeys as $key) {
508 2
            $fileMeta = $this->cache->get($key, true);
509
510 2
            if ( ! $this->isExpired($fileMeta)) {
511 1
                continue;
512
            }
513
514 2
            $cacheDeleted = $this->cache->delete($key);
515
516 2
            if ( ! $cacheDeleted) {
517 1
                continue;
518
            }
519
520 1
            if (file_exists($fileMeta['file_path']) && is_writable($fileMeta['file_path'])) {
521 1
                unlink($fileMeta['file_path']);
522
            }
523
524 1
            $deleted[] = $fileMeta;
525
        }
526
527 2
        return $deleted;
528
    }
529
530
    /**
531
     * No other methods are allowed.
532
     *
533
     * @param string $method
534
     * @param array  $params
535
     *
536
     * @return HttpResponse|BinaryFileResponse
537
     */
538 1
    public function __call(string $method, array $params)
539
    {
540 1
        return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
541
    }
542
}
543