Completed
Push — master ( 620d64...611391 )
by Ankit
02:13
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 TusPhp\Request;
7
use TusPhp\Response;
8
use TusPhp\Cache\Cacheable;
9
use TusPhp\Exception\FileException;
10
use TusPhp\Exception\ConnectionException;
11
use TusPhp\Exception\OutOfRangeException;
12
use Illuminate\Http\Response as HttpResponse;
13
use Symfony\Component\HttpFoundation\BinaryFileResponse;
14
15
class Server extends AbstractTus
16
{
17
    /** @const Tus Creation Extension */
18
    const TUS_EXTENSION_CREATION = 'creation';
19
20
    /** @const Tus Termination Extension */
21
    const TUS_EXTENSION_TERMINATION = 'termination';
22
23
    /** @const Tus Checksum Extension */
24
    const TUS_EXTENSION_CHECKSUM = 'checksum';
25
26
    /** @const 460 Checksum Mismatch */
27
    const HTTP_CHECKSUM_MISMATCH = 460;
28
29
    /** @var Request */
30
    protected $request;
31
32
    /** @var Response */
33
    protected $response;
34
35
    /** @var string */
36
    protected $uploadDir;
37
38
    /**
39
     * TusServer constructor.
40
     *
41
     * @param Cacheable|string $cacheAdapter
42
     */
43 3
    public function __construct($cacheAdapter = 'file')
44
    {
45 3
        $this->request   = new Request;
46 3
        $this->response  = new Response;
47 3
        $this->uploadDir = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'uploads';
48
49 3
        $this->setCache($cacheAdapter);
50 3
    }
51
52
    /**
53
     * Set upload dir.
54
     *
55
     * @param string $path
56
     *
57
     * @return void
58
     */
59 1
    public function setUploadDir(string $path)
60
    {
61 1
        $this->uploadDir = $path;
62 1
    }
63
64
    /**
65
     * Get upload dir.
66
     *
67
     * @return string
68
     */
69 1
    public function getUploadDir() : string
70
    {
71 1
        return $this->uploadDir;
72
    }
73
74
    /**
75
     * Get request.
76
     *
77
     * @return Request
78
     */
79 1
    public function getRequest() : Request
80
    {
81 1
        return $this->request;
82
    }
83
84
    /**
85
     * Get request.
86
     *
87
     * @return Response
88
     */
89 1
    public function getResponse() : Response
90
    {
91 1
        return $this->response;
92
    }
93
94
    /**
95
     * Handle all HTTP request.
96
     *
97
     * @return null|HttpResponse
98
     */
99 2
    public function serve()
100
    {
101 2
        $method = $this->getRequest()->method();
102
103 2
        if ( ! in_array($method, $this->request->allowedHttpVerbs())) {
104 1
            return $this->response->send(null, HttpResponse::HTTP_METHOD_NOT_ALLOWED);
105
        }
106
107 1
        $method = 'handle' . ucfirst(strtolower($method));
108
109 1
        $this->{$method}();
110
111 1
        $this->exit();
112 1
    }
113
114
    /**
115
     * Exit from current php process.
116
     *
117
     * @codeCoverageIgnore
118
     */
119
    protected function exit()
120
    {
121
        exit(0);
122
    }
123
124
    /**
125
     * Handle OPTIONS request.
126
     *
127
     * @return HttpResponse
128
     */
129 1
    protected function handleOptions() : HttpResponse
130
    {
131 1
        return $this->response->send(
132 1
            null,
133 1
            HttpResponse::HTTP_OK,
134
            [
135 1
                'Allow' => $this->request->allowedHttpVerbs(),
136 1
                'Tus-Version' => self::TUS_PROTOCOL_VERSION,
137 1
                'Tus-Extension' => implode(',', [
138 1
                    self::TUS_EXTENSION_CREATION,
139 1
                    self::TUS_EXTENSION_TERMINATION,
140 1
                    self::TUS_EXTENSION_CHECKSUM,
141
                ]),
142 1
                'Tus-Checksum-Algorithm' => $this->getSupportedHashAlgorithms(),
143
            ]
144
        );
145
    }
146
147
    /**
148
     * Handle HEAD request.
149
     *
150
     * @return HttpResponse
151
     */
152 3
    protected function handleHead() : HttpResponse
153
    {
154 3
        $checksum = $this->request->checksum();
155
156 3
        if ( ! $this->cache->get($checksum)) {
157 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
158
        }
159
160 2
        $offset = $this->cache->get($checksum)['offset'] ?? false;
161
162 2
        if (false === $offset) {
163 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
164
        }
165
166 1
        return $this->response->send(null, HttpResponse::HTTP_OK, [
167 1
            'Upload-Offset' => (int) $offset,
168 1
            'Cache-Control' => 'no-store',
169 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
170
        ]);
171
    }
172
173
    /**
174
     * Handle POST request.
175
     *
176
     * @return HttpResponse
177
     */
178 2
    protected function handlePost() : HttpResponse
179
    {
180 2
        $fileName = $this->getRequest()->extractFileName();
181
182 2
        if (empty($fileName)) {
183 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
184
        }
185
186 1
        $checksum = $this->getUploadChecksum();
187 1
        $location = $this->getRequest()->url() . '/' . basename($this->uploadDir) . '/' . $fileName;
188
189 1
        $file = $this->buildFile([
190 1
            'name' => $fileName,
191 1
            'offset' => 0,
192 1
            'size' => $this->getRequest()->header('Upload-Length'),
193 1
            'file_path' => $this->uploadDir . DIRECTORY_SEPARATOR . $fileName,
194 1
            'location' => $location,
195 1
        ])->setChecksum($checksum);
196
197 1
        $this->cache->set($checksum, $file->details());
198
199 1
        return $this->response->send(
200 1
            ['data' => ['checksum' => $checksum]],
201 1
            HttpResponse::HTTP_CREATED,
202
            [
203 1
                'Location' => $location,
204 1
                'Upload-Expires' => $this->cache->get($checksum)['expires_at'],
205 1
                'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
206
            ]
207
        );
208
    }
209
210
    /**
211
     * Handle PATCH request.
212
     *
213
     * @return HttpResponse
214
     */
215 6
    protected function handlePatch() : HttpResponse
216
    {
217 6
        $checksum = $this->request->checksum();
218
219 6
        if ( ! $this->cache->get($checksum)) {
220 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
221
        }
222
223 5
        $meta = $this->cache->get($checksum);
224 5
        $file = $this->buildFile($meta);
225
226
        try {
227 5
            $fileSize = $file->getFileSize();
228 5
            $offset   = $file->setChecksum($checksum)->upload($fileSize);
229
230
            // If upload is done, verify checksum.
231 2
            if ($offset === $fileSize && $checksum !== $this->getUploadChecksum()) {
232 2
                return $this->response->send(null, self::HTTP_CHECKSUM_MISMATCH);
233
            }
234 3
        } catch (FileException $e) {
235 1
            return $this->response->send($e->getMessage(), HttpResponse::HTTP_UNPROCESSABLE_ENTITY);
236 2
        } catch (OutOfRangeException $e) {
237 1
            return $this->response->send(null, HttpResponse::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE);
238 1
        } catch (ConnectionException $e) {
239 1
            return $this->response->send(null, HttpResponse::HTTP_CONTINUE);
240
        }
241
242 1
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
243 1
            'Upload-Expires' => $this->cache->get($checksum)['expires_at'],
244 1
            'Upload-Offset' => $offset,
245 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
246
        ]);
247
    }
248
249
    /**
250
     * Handle GET request.
251
     *
252
     * @return BinaryFileResponse|HttpResponse
253
     */
254 4
    protected function handleGet()
255
    {
256 4
        $checksum = $this->request->checksum();
257
258 4
        if (empty($checksum)) {
259 1
            return $this->response->send('400 bad request.', HttpResponse::HTTP_BAD_REQUEST);
260
        }
261
262 3
        $fileMeta = $this->cache->get($checksum);
263
264 3
        if ( ! $fileMeta) {
265 1
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
266
        }
267
268 2
        $resource = $fileMeta['file_path'] ?? null;
269 2
        $fileName = $fileMeta['name'] ?? null;
270
271 2
        if ( ! $resource || ! file_exists($resource)) {
272 1
            return $this->response->send('404 upload not found.', HttpResponse::HTTP_NOT_FOUND);
273
        }
274
275 1
        return $this->response->download($resource, $fileName);
276
    }
277
278
    /**
279
     * Handle DELETE request.
280
     *
281
     * @return HttpResponse
282
     */
283 3
    protected function handleDelete() : HttpResponse
284
    {
285 3
        $checksum = $this->request->checksum();
286 3
        $fileMeta = $this->cache->get($checksum);
287 3
        $resource = $fileMeta['file_path'] ?? null;
288
289 3
        if ( ! $resource) {
290 1
            return $this->response->send(null, HttpResponse::HTTP_NOT_FOUND);
291
        }
292
293 2
        $isDeleted = $this->cache->delete($checksum);
294
295 2
        if ( ! $isDeleted || ! file_exists($resource)) {
296 1
            return $this->response->send(null, HttpResponse::HTTP_GONE);
297
        }
298
299 1
        return $this->response->send(null, HttpResponse::HTTP_NO_CONTENT, [
300 1
            'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
301 1
            'Tus-Extension' => self::TUS_EXTENSION_TERMINATION,
302
        ]);
303
    }
304
305
    /**
306
     * Build file object.
307
     *
308
     * @param array $meta
309
     *
310
     * @return File
311
     */
312 1
    protected function buildFile(array $meta) : File
313
    {
314 1
        return (new File($meta['name'], $this->cache))
315 1
            ->setMeta($meta['offset'], $meta['size'], $meta['file_path'], $meta['location']);
316
    }
317
318
    /**
319
     * Get list of supported hash algorithms.
320
     *
321
     * @return string
322
     */
323 1
    protected function getSupportedHashAlgorithms()
324
    {
325 1
        $supportedAlgorithms = hash_algos();
326
327 1
        $algorithms = [];
328 1
        foreach ($supportedAlgorithms as $hashAlgo) {
329 1
            if (false !== strpos($hashAlgo, ',')) {
330 1
                $algorithms[] = "'{$hashAlgo}'";
331
            } else {
332 1
                $algorithms[] = $hashAlgo;
333
            }
334
        }
335
336 1
        return implode(',', $algorithms);
337
    }
338
339
    /**
340
     * Verify and get upload checksum from header.
341
     *
342
     * @return string|HttpResponse
343
     */
344 4
    protected function getUploadChecksum()
345
    {
346 4
        $checksumHeader = $this->getRequest()->header('Upload-Checksum');
347
348 4
        if ( ! $checksumHeader) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $checksumHeader of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
349 1
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
350
        }
351
352 3
        list($checksumAlgorithm, $checksum) = explode(' ', $checksumHeader);
353
354 3
        $checksum = base64_decode($checksum);
355
356 3
        if ( ! in_array($checksumAlgorithm, hash_algos()) || false === $checksum) {
357 2
            return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
358
        }
359
360 1
        return $checksum;
361
    }
362
363
    /**
364
     * No other methods are allowed.
365
     *
366
     * @param string $method
367
     * @param array  $params
368
     *
369
     * @return HttpResponse|BinaryFileResponse
370
     */
371 1
    public function __call(string $method, array $params)
372
    {
373 1
        return $this->response->send(null, HttpResponse::HTTP_BAD_REQUEST);
374
    }
375
}
376