Passed
Push — develop ( f30815...940473 )
by Henry
03:36
created

MediaRequestHandler::respondRangeNotSatisfiable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 4
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 1
rs 10
1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Controllers;
12
13
use Exception;
14
use Divergence\Models\Media\Media;
1 ignored issue
show
Bug introduced by
The type Divergence\Models\Media\Media was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use Divergence\Models\ActiveRecord;
16
use Divergence\Responders\Response;
17
use Divergence\Responders\JsonBuilder;
18
use Divergence\Responders\EmptyBuilder;
19
use Divergence\Responders\EmptyResponse;
20
use Divergence\Responders\MediaBuilder;
21
use Psr\Http\Message\ResponseInterface;
22
use Divergence\Responders\MediaResponse;
23
use GuzzleHttp\Psr7\ServerRequest;
24
use Psr\Http\Message\ServerRequestInterface;
25
26
class MediaRequestHandler extends RecordsRequestHandler
27
{
28
    // RecordRequestHandler configuration
29
    public static $recordClass = Media::class;
30
    public $accountLevelRead = false;
31
    public $accountLevelBrowse = 'User';
32
    public $accountLevelWrite = 'User';
33
    public $accountLevelAPI = false;
34
    public $browseLimit = 100;
35
    public $browseOrder = ['ID' => 'DESC'];
36
37
    // MediaRequestHandler configuration
38
    public $defaultPage = 'browse';
39
    public $defaultThumbnailWidth = 100;
40
    public $defaultThumbnailHeight = 100;
41
    public $uploadFileFieldName = 'mediaFile';
42
    public $responseMode = 'html';
43
44
    public static $inputStream = 'php://input'; // this is a setting so that unit tests can provide a fake stream :)
45
46
    public $searchConditions = [
47
        'Caption' => [
48
            'qualifiers' => ['any','caption']
49
            ,'points' => 2
50
            ,'sql' => 'Caption LIKE "%%%s%%"',
51
        ]
52
        ,'CaptionLike' => [
53
            'qualifiers' => ['caption-like']
54
            ,'points' => 2
55
            ,'sql' => 'Caption LIKE "%s"',
56
        ]
57
        ,'CaptionNot' => [
58
            'qualifiers' => ['caption-not']
59
            ,'points' => 2
60
            ,'sql' => 'Caption NOT LIKE "%%%s%%"',
61
        ]
62
        ,'CaptionNotLike' => [
63
            'qualifiers' => ['caption-not-like']
64
            ,'points' => 2
65
            ,'sql' => 'Caption NOT LIKE "%s"',
66
        ],
67
    ];
68
69
    private ?ServerRequest $request;
70
71 22
    public function handle(ServerRequestInterface $request): ResponseInterface
72
    {
73 22
        $this->request = $request;
74
75
        // handle json response mode
76 22
        if ($this->peekPath() == 'json') {
77 11
            $this->shiftPath();
78 11
            $this->responseBuilder = JsonBuilder::class;
79
        }
80
81
        // handle action
82 22
        switch ($action = $this->shiftPath()) {
83
84 22
            case 'upload':
85
                {
86 7
                    return $this->handleUploadRequest();
87
                }
88
89 15
            case 'open':
90
                {
91
                    $mediaID = $this->shiftPath();
92
93
                    return $this->handleMediaRequest($mediaID);
94
                }
95
96 15
            case 'download':
97
                {
98 1
                    $mediaID = $this->shiftPath();
99 1
                    if ($filename = $this->shiftPath()) {
100
                        $filename = urldecode($filename);
101
                    }
102 1
                    return $this->handleDownloadRequest($mediaID, $filename);
103
                }
104
105 14
            case 'info':
106
                {
107 1
                    $mediaID = $this->shiftPath();
108
109 1
                    return $this->handleInfoRequest($mediaID);
110
                }
111
112 13
            case 'caption':
113
                {
114
                    $mediaID = $this->shiftPath();
115
116
                    return $this->handleCaptionRequest($mediaID);
117
                }
118
119 13
            case 'delete':
120
                {
121
                    $mediaID = $this->shiftPath();
122
                    return $this->handleDeleteRequest($mediaID);
123
                }
124
125 13
            case 'thumbnail':
126
                {
127
                    return $this->handleThumbnailRequest();
128
                }
129
130
            case false:
131 12
            case '':
132 12
            case 'browse':
133
                {
134 1
                    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
135
                        return $this->handleUploadRequest();
136
                    }
137
138 1
                    return $this->handleBrowseRequest();
139
                }
140
141
            default:
142
                {
143 12
                    if (ctype_digit($action)) {
144 12
                        return $this->handleMediaRequest($action);
145
                    } else {
146
                        return parent::handleRecordsRequest($action);
147
                    }
148
                }
149
        }
150
    }
151
152
153 7
    public function handleUploadRequest($options = []): ResponseInterface
154
    {
155 7
        $this->checkUploadAccess();
156
157 7
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
158
            // init options
159 5
            $options = array_merge([
160 5
                'fieldName' => $this->uploadFileFieldName,
161 5
            ], $options);
162
163
164
            // check upload
165 5
            if (empty($_FILES[$options['fieldName']])) {
166 1
                return $this->throwUploadError('You did not select a file to upload');
167
            }
168
169
            // handle upload errors
170 4
            if ($_FILES[$options['fieldName']]['error'] != UPLOAD_ERR_OK) {
171 4
                switch ($_FILES[$options['fieldName']]['error']) {
172 4
                    case UPLOAD_ERR_NO_FILE:
173 1
                        return $this->throwUploadError('You did not select a file to upload');
174
175 3
                    case UPLOAD_ERR_INI_SIZE:
176 2
                    case UPLOAD_ERR_FORM_SIZE:
177 1
                        return $this->throwUploadError('Your file exceeds the maximum upload size. Please try again with a smaller file.');
178
179 2
                    case UPLOAD_ERR_PARTIAL:
180 1
                        return $this->throwUploadError('Your file was only partially uploaded, please try again.');
181
182
                    default:
183 1
                        return $this->throwUploadError('There was an unknown problem while processing your upload, please try again.');
184
                }
185
            }
186
187
            // init caption
188
            if (!isset($options['Caption'])) {
189
                if (!empty($_REQUEST['Caption'])) {
190
                    $options['Caption'] = $_REQUEST['Caption'];
191
                } else {
192
                    $options['Caption'] = preg_replace('/\.[^.]+$/', '', $_FILES[$options['fieldName']]['name']);
193
                }
194
            }
195
196
            // create media
197
            try {
198
                $Media = Media::createFromUpload($_FILES[$options['fieldName']]['tmp_name'], $options);
199
            } catch (Exception $e) {
200
                return $this->throwUploadError($e->getMessage());
201
            }
202 2
        } elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
203 2
            $put = fopen(static::$inputStream, 'r'); // open input stream
204
205 2
            $tmp = tempnam('/tmp', 'dvr');  // use PHP to make a temporary file
206 2
            $fp = fopen($tmp, 'w'); // open write stream to temp file
207
208
            // write
209 2
            while ($data = fread($put, 1024)) {
210 2
                fwrite($fp, $data);
211
            }
212
213
            // close handles
214 2
            fclose($fp);
215 2
            fclose($put);
216
217
            // create media
218
            try {
219 2
                $Media = Media::createFromFile($tmp, $options);
220
            } catch (Exception $e) {
221 2
                return $this->throwUploadError('The file you uploaded is not of a supported media format');
222
            }
223
        } else {
224
            return $this->respond('upload');
225
        }
226
227
        // assign context
228 2
        if (!empty($_REQUEST['ContextClass']) && !empty($_REQUEST['ContextID'])) {
229
            if (!is_subclass_of($_REQUEST['ContextClass'], ActiveRecord::class)
230
                || !in_array($_REQUEST['ContextClass']::getStaticRootClass(), Media::$fields['ContextClass']['values'])
231
                || !is_numeric($_REQUEST['ContextID'])) {
232
                return $this->throwUploadError('Context is invalid');
233
            } elseif (!$Media->Context = $_REQUEST['ContextClass']::getByID($_REQUEST['ContextID'])) {
234
                return $this->throwUploadError('Context class not found');
235
            }
236
237
            $Media->save();
238
        }
239
240 2
        return $this->respond('uploadComplete', [
241 2
            'success' => (bool)$Media
242 2
            ,'data' => $Media,
243 2
        ]);
244
    }
245
246 2
    public function respondRangeNotSatisfiable(string $responseID, int $start, int $end, int $size): Response
247
    {
248 2
        $this->responseBuilder = EmptyBuilder::class;
249
250 2
        return $this->respondEmpty($responseID)
251 2
            ->withStatus(416) // Range Not Satisfiable
252 2
            ->withHeader('Content-Range', "bytes $start-$end/$size");
253
    }
254
255
    /**
256
     * Set caching headers
257
     *
258
     * @param Response $response
259
     * @return Response
260
     */
261 7
    public function setCache(Response $response): Response
262
    {
263 7
        $expires = 60*60*24*365;
264 7
        return $response->withHeader('Cache-Control', "public, max-age= $expires")
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...der('Pragma', 'public') could return the type GuzzleHttp\Psr7\MessageTrait&object which includes types incompatible with the type-hinted return Divergence\Responders\Response. Consider adding an additional type-check to rule them out.
Loading history...
265 7
        ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time()+$expires))
266 7
        ->withHeader('Pragma', 'public');
267
    }
268
269 9
    public function respondWithMedia(Media $Media, $variant, $responseID, $responseData = []): ResponseInterface
270
    {
271 9
        if ($this->responseBuilder != MediaBuilder::class) {
272
            throw new Exception('Media responses require MediaBuilder for putting together a response.');
273
        }
274 9
        $className = $this->responseBuilder;
275 9
        $responseBuilder = new $className($responseID, $responseData);
276
277 9
        $responseBuilder->setContentType($Media->MIMEType);
278
279
280 9
        $size = filesize($responseID);
281 9
        $length = $size;
282 9
        $start = 0;
283 9
        $end = $size - 1;
284
285
        // interpret range requests
286 9
        $_server = $this->request->getServerParams();
0 ignored issues
show
Bug introduced by
The method getServerParams() does not exist on null. ( Ignorable by Annotation )

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

286
        /** @scrutinizer ignore-call */ 
287
        $_server = $this->request->getServerParams();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
287 9
        if (!empty($_server['HTTP_RANGE'])) {
288 7
            $chunkStart = $start;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkStart is dead and can be removed.
Loading history...
289 7
            $chunkEnd = $end;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkEnd is dead and can be removed.
Loading history...
290
291 7
            list(, $range) = explode('=', $_server['HTTP_RANGE'], 2);
292
293
            // comma indicates multiple ranges which we currently do not support
294 7
            if (strpos($range, ',') !== false) {
295 1
                return $this->respondRangeNotSatisfiable($responseID, $start, $end, $size);
296
            }
297
298 6
            if ($range == '-') { // missing range start and end
299 1
                $range = '0-';
300
            }
301
302 6
            $range = explode('-', $range);
303 6
            $chunkStart = $range[0];
304 6
            $chunkEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
305
306
307 6
            $chunkEnd = ($chunkEnd > $end) ? $end : $chunkEnd;
308
309
            // requested content out of bounds
310 6
            if ($chunkStart > $chunkEnd || $chunkStart > $size - 1 || $chunkEnd >= $size) {
311 1
                return $this->respondRangeNotSatisfiable($responseID, $start, $end, $size);
312
            }
313
314 5
            $start = intval($chunkStart);
315 5
            $end = intval($chunkEnd);
316 5
            $length = $end - $start + 1;
317 5
            $responseBuilder->setRange($start, $end, $length);
318
        }
319
320
321 7
        $response = new MediaResponse($responseBuilder);
322 7
        $response = $this->setCache($response);
323
324
        // tell browser ranges are accepted
325 7
        $response = $response->withHeader('Accept-Ranges', 'bytes')
326 7
        // provide a unique ID for this media
327 7
        ->withHeader('ETag', 'media-'.$Media->ID.'-'.$variant);
328
329
        // if partial content provide proper response header
330 7
        if (isset($chunkStart)) {
331 5
            $response = $response->withStatus(206)
0 ignored issues
show
Bug introduced by
It seems like withStatus() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

331
            $response = $response->/** @scrutinizer ignore-call */ withStatus(206)
Loading history...
332 5
            ->withHeader('Content-Range', "bytes $start-$end/$size")
333 5
            ->withHeader('Content-Length', $length);
334
        } else {
335
            // range
336 2
            $filesize = filesize($Media->getFilesystemPath($variant));
337 2
            $end = $filesize - 1;
338 2
            $response = $response->withHeader('Content-Range', 'bytes 0-'.$end.'/'.$filesize)
339 2
                ->withHeader('Content-Length', $filesize);
340
        }
341
342 7
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type GuzzleHttp\Psr7\MessageTrait&object which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
343
    }
344
345 4
    public function respondEmpty($responseID, $responseData = [])
346
    {
347 4
        if ($this->responseBuilder != EmptyBuilder::class) {
348
            throw new Exception('Media responses require MediaBuilder for putting together a response.');
349
        }
350 4
        $className = $this->responseBuilder;
351 4
        $responseBuilder = new $className($responseID, $responseData);
352 4
        $response = new EmptyResponse($responseBuilder);
353 4
        return $response;
354
    }
355
356
357 12
    public function handleMediaRequest($mediaID): ResponseInterface
358
    {
359 12
        if (empty($mediaID)) {
360
            return $this->throwNotFoundError();
361
        }
362
363
        // get media
364
        try {
365 12
            $Media = Media::getById($mediaID);
366
        } catch (Exception $e) {
367
            return $this->throwUnauthorizedError();
368
        }
369
370 12
        if (!$Media) {
371 1
            return $this->throwNotFoundError();
372
        }
373
374 11
        if (!$this->checkReadAccess($Media)) {
375
            return $this->throwNotFoundError();
376
        }
377
378
379 11
        $_server = $this->request->getServerParams();
380
381 11
        if (isset($_server['HTTP_ACCEPT'])) {
382
            if ($_server['HTTP_ACCEPT'] == 'application/json') {
383
                $this->responseBuilder = JsonBuilder::class;
384
            }
385
        }
386
387 11
        if ($this->responseBuilder == JsonBuilder::class) {
388 1
            return $this->respond('media', [
389 1
                'success' => true
390 1
                ,'data' => $Media,
391 1
            ]);
392
        } else {
393
394
            // determine variant
395 10
            if ($variant = $this->shiftPath()) {
396
                if (!$Media->isVariantAvailable($variant)) {
397
                    return $this->throwNotFoundError();
398
                }
399
            } else {
400 10
                $variant = 'original';
401
            }
402
403
            // initialize response
404 10
            $this->responseBuilder = MediaBuilder::class;
405 10
            set_time_limit(0);
406 10
            $filePath = $Media->getFilesystemPath($variant);
407
408
            // media are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
409 10
            if (!empty($_server['HTTP_IF_NONE_MATCH']) || !empty($_server['HTTP_IF_MODIFIED_SINCE'])) {
410 2
                $this->responseBuilder = EmptyBuilder::class;
411 2
                $response = $this->respondEmpty($filePath);
412 2
                $response->withDefaults(304);
413
414 2
                return $response;
415
            }
416
417
418 8
            return $this->respondWithMedia($Media, $variant, $filePath);
419
        }
420
    }
421
422 1
    public function handleInfoRequest($mediaID): ResponseInterface
423
    {
424 1
        if (empty($mediaID) || !is_numeric($mediaID)) {
425
            $this->throwNotFoundError();
426
        }
427
428
        // get media
429
        try {
430 1
            $Media = Media::getById($mediaID);
431
        } catch (Exception $e) {
432
            return $this->throwUnauthorizedError();
433
        }
434
435 1
        if (!$Media) {
436
            return $this->throwNotFoundError();
437
        }
438
439 1
        if (!$this->checkReadAccess($Media)) {
440
            return $this->throwUnauthorizedError();
441
        }
442
443 1
        return parent::handleRecordRequest($Media);
444
    }
445
446 1
    public function handleDownloadRequest($media_id, $filename = false): ResponseInterface
447
    {
448 1
        if (empty($media_id) || !is_numeric($media_id)) {
449
            $this->throwNotFoundError();
450
        }
451
452
        // get media
453
        try {
454 1
            $Media = Media::getById($media_id);
455
        } catch (Exception $e) {
456
            return $this->throwUnauthorizedError();
457
        }
458
459
460 1
        if (!$Media) {
461
            return $this->throwNotFoundError();
462
        }
463
464 1
        if (!$this->checkReadAccess($Media)) {
465
            return $this->throwUnauthorizedError();
466
        }
467
468 1
        $filePath = $Media->getFilesystemPath('original');
469
470 1
        $this->responseBuilder = MediaBuilder::class;
471 1
        $response = $this->respondWithMedia($Media, 'original', $filePath);
472
473 1
        $response = $response->withHeader('Content-Disposition', 'attachment; filename="'.($filename ? $filename : $filePath).'"');
474
475 1
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type GuzzleHttp\Psr7\MessageTrait&object which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
476
    }
477
478
    public function handleCaptionRequest($media_id): ResponseInterface
479
    {
480
        // require authentication
481
        $GLOBALS['Session']->requireAccountLevel('Staff');
482
483
        if (empty($media_id) || !is_numeric($media_id)) {
484
            return $this->throwNotFoundError();
485
        }
486
487
        // get media
488
        try {
489
            $Media = Media::getById($media_id);
490
        } catch (Exception $e) {
491
            return $this->throwUnauthorizedError();
492
        }
493
494
495
        if (!$Media) {
496
            $this->throwNotFoundError();
497
        }
498
499
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
500
            $Media->Caption = $_REQUEST['Caption'];
501
            $Media->save();
502
503
            return $this->respond('mediaCaptioned', [
504
                'success' => true
505
                ,'data' => $Media,
506
            ]);
507
        }
508
509
        return $this->respond('mediaCaption', [
510
            'data' => $Media,
511
        ]);
512
    }
513
514
    public function handleDeleteRequest(ActiveRecord $Record): ResponseInterface
515
    {
516
        // require authentication
517
        $GLOBALS['Session']->requireAccountLevel('Staff');
518
519
        if ($mediaID = $this->peekPath()) {
520
            $mediaIDs = [$mediaID];
521
        } elseif (!empty($_REQUEST['mediaID'])) {
522
            $mediaIDs = [$_REQUEST['mediaID']];
523
        } elseif (is_array($_REQUEST['media'])) {
524
            $mediaIDs = $_REQUEST['media'];
525
        }
526
527
        $deleted = [];
528
        foreach ($mediaIDs as $mediaID) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mediaIDs does not seem to be defined for all execution paths leading up to this point.
Loading history...
529
            if (!is_numeric($mediaID)) {
530
                continue;
531
            }
532
533
            // get media
534
            $Media = Media::getByID($mediaID);
535
536
            if (!$Media) {
537
                return $this->throwNotFoundError();
538
            }
539
540
            if ($Media->destroy()) {
541
                $deleted[] = $Media;
542
            }
543
        }
544
545
        return $this->respond('mediaDeleted', [
546
            'success' => true
547
            ,'data' => $deleted,
548
        ]);
549
    }
550
551
    public function handleThumbnailRequest(Media $Media = null): ResponseInterface
552
    {
553
        // send caching headers
554
        if (!headers_sent()) {
555
            // @codeCoverageIgnoreStart
556
            $expires = 60*60*24*365;
557
            header("Cache-Control: public, max-age=$expires");
558
            header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time()+$expires));
559
            header('Pragma: public');
560
            // @codeCoverageIgnoreEnd
561
        }
562
563
        // thumbnails are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
564
        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
565
            header('HTTP/1.0 304 Not Modified');
566
            exit();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
567
        }
568
569
        // get media
570
        if (!$Media) {
571
            if (!$mediaID = $this->shiftPath()) {
572
                return $this->throwNotFoundError();
573
            } elseif (!$Media = Media::getByID($mediaID)) {
574
                return $this->throwNotFoundError();
575
            }
576
        }
577
578
        // get format
579
        if (preg_match('/^(\d+)x(\d+)(x([0-9A-F]{6})?)?$/i', $this->peekPath(), $matches)) {
0 ignored issues
show
Bug introduced by
It seems like $this->peekPath() can also be of type false; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

579
        if (preg_match('/^(\d+)x(\d+)(x([0-9A-F]{6})?)?$/i', /** @scrutinizer ignore-type */ $this->peekPath(), $matches)) {
Loading history...
580
            $this->shiftPath();
581
            $maxWidth = $matches[1];
582
            $maxHeight = $matches[2];
583
            $fillColor = !empty($matches[4]) ? $matches[4] : false;
584
        } else {
585
            $maxWidth = $this->defaultThumbnailWidth;
586
            $maxHeight = $this->defaultThumbnailHeight;
587
            $fillColor = false;
588
        }
589
590
        if ($this->peekPath() == 'cropped') {
591
            $this->shiftPath();
592
            $cropped = true;
593
        } else {
594
            $cropped = false;
595
        }
596
597
        // get thumbnail media
598
        try {
599
            $thumbPath = $Media->getThumbnail($maxWidth, $maxHeight, $fillColor, $cropped);
600
        } catch (Exception $e) {
601
            return $this->throwNotFoundError();
602
        }
603
604
        // emit
605
        if (!headers_sent()) {
606
            // @codeCoverageIgnoreStart
607
            header("ETag: media-$Media->ID-$maxWidth-$maxHeight-$fillColor-$cropped");
608
            header("Content-Type: $Media->ThumbnailMIMEType");
609
            header('Content-Length: '.filesize($thumbPath));
610
            readfile($thumbPath);
611
            // @codeCoverageIgnoreEnd
612
        }
613
        exit();
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
614
    }
615
616
617 1
    public function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = []): ResponseInterface
618
    {
619
        // apply tag filter
620 1
        if (!empty($_REQUEST['tag'])) {
621
            // get tag
622
            if (!$Tag = Tag::getByHandle($_REQUEST['tag'])) {
0 ignored issues
show
Bug introduced by
The type Divergence\Controllers\Tag was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
623
                return $this->throwNotFoundError();
624
            }
625
626
            $conditions[] = 'ID IN (SELECT ContextID FROM tag_items WHERE TagID = '.$Tag->ID.' AND ContextClass = "Product")';
627
        }
628
629
630
        // apply context filter
631 1
        if (!empty($_REQUEST['ContextClass'])) {
632
            $conditions['ContextClass'] = $_REQUEST['ContextClass'];
633
        }
634
635 1
        if (!empty($_REQUEST['ContextID']) && is_numeric($_REQUEST['ContextID'])) {
636
            $conditions['ContextID'] = $_REQUEST['ContextID'];
637
        }
638
639 1
        return parent::handleBrowseRequest($options, $conditions, $responseID, $responseData);
640
    }
641
642
643
644
    public function handleMediaDeleteRequest(): ResponseInterface
645
    {
646
        // sanity check
647
        if (empty($_REQUEST['media']) || !is_array($_REQUEST['media'])) {
648
            return $this->throwNotFoundError();
649
        }
650
651
        // retrieve photos
652
        $media_array = [];
653
        foreach ($_REQUEST['media'] as $media_id) {
654
            if (!is_numeric($media_id)) {
655
                return $this->throwNotFoundError();
656
            }
657
658
            if ($Media = Media::getById($media_id)) {
659
                $media_array[$Media->ID] = $Media;
660
661
                if (!$this->checkWriteAccess($Media)) {
662
                    return $this->throwUnauthorizedError();
663
                }
664
            }
665
        }
666
667
        // delete
668
        $deleted = [];
669
        foreach ($media_array as $media_id => $Media) {
670
            if ($Media->delete()) {
671
                $deleted[] = $media_id;
672
            }
673
        }
674
675
        return $this->respond('mediaDeleted', [
676
            'success' => true
677
            ,'deleted' => $deleted,
678
        ]);
679
    }
680
681 7
    public function checkUploadAccess()
682
    {
683 7
        return true;
684
    }
685
686 5
    public function throwUploadError($error): ResponseInterface
687
    {
688 5
        return $this->respond('error', [
689 5
            'success' => false,
690 5
            'failed' => [
691 5
                'errors'	=>	$error,
692 5
            ],
693 5
        ]);
694
    }
695
}
696