Test Setup Failed
Push — develop ( ed9389...5203c7 )
by Henry
02:49
created

MediaRequestHandler::respondWithMedia()   C

Complexity

Conditions 12
Paths 38

Size

Total Lines 80
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 99.9666

Importance

Changes 0
Metric Value
cc 12
eloc 49
c 0
b 0
f 0
nc 38
nop 4
dl 0
loc 80
ccs 5
cts 33
cp 0.1515
crap 99.9666
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 9
            ,'points' => 2
65
            ,'sql' => 'Caption NOT LIKE "%s"',
66
        ],
67 9
    ];
68 9
69 9
    private ?ServerRequest $request;
70
71
    public function handle(ServerRequestInterface $request): ResponseInterface
72
    {
73 9
        $this->request = $request;
74
75 9
        // handle json response mode
76
        if ($this->peekPath() == 'json') {
77 6
            $this->shiftPath();
78
            $this->responseBuilder = JsonBuilder::class;
79
        }
80 3
81
        // handle action
82
        switch ($action = $this->shiftPath()) {
83
84
            case 'upload':
85
                {
86
                    return $this->handleUploadRequest();
87 3
                }
88
89
            case 'open':
90
                {
91
                    $mediaID = $this->shiftPath();
92
93
                    return $this->handleMediaRequest($mediaID);
94
                }
95 3
96
            case 'download':
97 1
                {
98
                    $mediaID = $this->shiftPath();
99 1
                    if ($filename = $this->shiftPath()) {
100
                        $filename = urldecode($filename);
101
                    }
102 2
                    return $this->handleDownloadRequest($mediaID, $filename);
103
                }
104
105
            case 'info':
106
                {
107
                    $mediaID = $this->shiftPath();
108
109 2
                    return $this->handleInfoRequest($mediaID);
110
                }
111
112
            case 'caption':
113
                {
114
                    $mediaID = $this->shiftPath();
115 2
116
                    return $this->handleCaptionRequest($mediaID);
117
                }
118
119
            case 'delete':
120
                {
121 1
                    $mediaID = $this->shiftPath();
122 1
                    return $this->handleDeleteRequest($mediaID);
123
                }
124 1
125
            case 'thumbnail':
126
                {
127
                    return $this->handleThumbnailRequest();
128 1
                }
129
130
            case false:
131
            case '':
132
            case 'browse':
133 1
                {
134 1
                    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
135
                        return $this->handleUploadRequest();
136
                    }
137
138
                    return $this->handleBrowseRequest();
139
                }
140
141
            default:
142
                {
143 6
                    if (ctype_digit($action)) {
144
                        return $this->handleMediaRequest($action);
145 6
                    } else {
146
                        return parent::handleRecordsRequest($action);
147 6
                    }
148
                }
149 5
        }
150 5
    }
151 5
152
153
    public function handleUploadRequest($options = []): ResponseInterface
154
    {
155 5
        $this->checkUploadAccess();
156 1
157
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
158
            // init options
159
            $options = array_merge([
160 4
                'fieldName' => $this->uploadFileFieldName,
161 4
            ], $options);
162 4
163 1
164
            // check upload
165 3
            if (empty($_FILES[$options['fieldName']])) {
166 2
                return $this->throwUploadError('You did not select a file to upload');
167 1
            }
168
169 2
            // handle upload errors
170 1
            if ($_FILES[$options['fieldName']]['error'] != UPLOAD_ERR_OK) {
171
                switch ($_FILES[$options['fieldName']]['error']) {
172
                    case UPLOAD_ERR_NO_FILE:
173 1
                        return $this->throwUploadError('You did not select a file to upload');
174
175
                    case UPLOAD_ERR_INI_SIZE:
176
                    case UPLOAD_ERR_FORM_SIZE:
177
                        return $this->throwUploadError('Your file exceeds the maximum upload size. Please try again with a smaller file.');
178
179
                    case UPLOAD_ERR_PARTIAL:
180
                        return $this->throwUploadError('Your file was only partially uploaded, please try again.');
181
182
                    default:
183
                        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 1
                    $options['Caption'] = preg_replace('/\.[^.]+$/', '', $_FILES[$options['fieldName']]['name']);
193 1
                }
194
            }
195 1
196 1
            // create media
197
            try {
198
                $Media = Media::createFromUpload($_FILES[$options['fieldName']]['tmp_name'], $options);
199 1
            } catch (Exception $e) {
200 1
                return $this->throwUploadError($e->getMessage());
201
            }
202
        } elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
203
            $put = fopen(static::$inputStream, 'r'); // open input stream
204 1
205 1
            $tmp = tempnam('/tmp', 'dvr');  // use PHP to make a temporary file
206
            $fp = fopen($tmp, 'w'); // open write stream to temp file
207
208
            // write
209 1
            while ($data = fread($put, 1024)) {
210
                fwrite($fp, $data);
211 1
            }
212
213
            // close handles
214
            fclose($fp);
215
            fclose($put);
216
217
            // create media
218 1
            try {
219
                $Media = Media::createFromFile($tmp, $options);
220
            } catch (Exception $e) {
221
                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
        if (!empty($_REQUEST['ContextClass']) && !empty($_REQUEST['ContextID'])) {
229
            if (!is_subclass_of($_REQUEST['ContextClass'], ActiveRecord::class)
230 1
                || !in_array($_REQUEST['ContextClass']::getStaticRootClass(), Media::$fields['ContextClass']['values'])
231 1
                || !is_numeric($_REQUEST['ContextID'])) {
232 1
                return $this->throwUploadError('Context is invalid');
233 1
            } elseif (!$Media->Context = $_REQUEST['ContextClass']::getByID($_REQUEST['ContextID'])) {
234
                return $this->throwUploadError('Context class not found');
235
            }
236
237 1
            $Media->save();
238
        }
239 1
240
        return $this->respond('uploadComplete', [
241
            'success' => (bool)$Media
242
            ,'data' => $Media,
243
        ]);
244
    }
245 1
246
    /**
247
     * Set caching headers
248
     *
249
     * @param Response $response
250 1
     * @return Response
251
     */
252
    public function setCache(Response $response): Response
253
    {
254 1
        $expires = 60*60*24*365;
255
        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...
256
        ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time()+$expires))
257
        ->withHeader('Pragma', 'public');
258 1
    }
259
260
    public function respondWithMedia(Media $Media, $variant, $responseID, $responseData = []): ResponseInterface
261
    {
262
        if ($this->responseBuilder != MediaBuilder::class) {
263
            throw new Exception('Media responses require MediaBuilder for putting together a response.');
264 1
        }
265 1
        $className = $this->responseBuilder;
266 1
        $responseBuilder = new $className($responseID, $responseData);
267 1
268 1
        $responseBuilder->setContentType($Media->MIMEType);
269
270
271
        $fp = fopen($responseID, 'rb');
0 ignored issues
show
Unused Code introduced by
The assignment to $fp is dead and can be removed.
Loading history...
272
        $size = filesize($responseID);
273
        $length = $size;
274
        $start = 0;
275
        $end = $size - 1;
276
277
        // interpret range requests
278
        $_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

278
        /** @scrutinizer ignore-call */ 
279
        $_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...
279
        if (!empty($_server['HTTP_RANGE'])) {
280
            $chunkStart = $start;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkStart is dead and can be removed.
Loading history...
281
            $chunkEnd = $end;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkEnd is dead and can be removed.
Loading history...
282
283
            list(, $range) = explode('=', $_server['HTTP_RANGE'], 2);
284
285
            if (strpos($range, ',') !== false) {
286
                $this->responseBuilder = EmptyBuilder::class;
287
288
                return $this->respondEmpty($responseID)
289
                    ->withStatus(416) // Range Not Satisfiable
290
                    ->withHeader('Content-Range', "bytes $start-$end/$size");
291
            }
292
293
            if ($range == '-') { // missing range start and end
294
                $range = '0-';
295
            }
296
297
            $range = explode('-', $range);
298
            $chunkStart = $range[0];
299
            $chunkEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
300
301
302
            $chunkEnd = ($chunkEnd > $end) ? $end : $chunkEnd;
303
            if ($chunkStart > $chunkEnd || $chunkStart > $size - 1 || $chunkEnd >= $size) {
304
                $this->responseBuilder = EmptyBuilder::class;
305
306
                return $this->respondEmpty($responseID)
307
                    ->withStatus(416) // Range Not Satisfiable
308
                    ->withHeader('Content-Range', "bytes $start-$end/$size");
309
            }
310
311
            $start = intval($chunkStart);
312
            $end = intval($chunkEnd);
313
            $length = $end - $start + 1;
314
            $responseBuilder->setRange($start, $end, $length);
315
        }
316
317
318
        $response = new MediaResponse($responseBuilder);
319
        $response = $this->setCache($response);
320
321
        // tell browser ranges are accepted
322
        $response = $response->withHeader('Accept-Ranges', 'bytes')
323
        // provide a unique ID for this media
324
        ->withHeader('ETag', 'media-'.$Media->ID.'-'.$variant);
325
326
        // if partial content provide proper response header
327
        if (isset($chunkStart)) {
328
            $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

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

576
        if (preg_match('/^(\d+)x(\d+)(x([0-9A-F]{6})?)?$/i', /** @scrutinizer ignore-type */ $this->peekPath(), $matches)) {
Loading history...
577
            $this->shiftPath();
578
            $maxWidth = $matches[1];
579
            $maxHeight = $matches[2];
580
            $fillColor = !empty($matches[4]) ? $matches[4] : false;
581
        } else {
582
            $maxWidth = $this->defaultThumbnailWidth;
583
            $maxHeight = $this->defaultThumbnailHeight;
584
            $fillColor = false;
585
        }
586
587
        if ($this->peekPath() == 'cropped') {
588
            $this->shiftPath();
589 1
            $cropped = true;
590
        } else {
591
            $cropped = false;
592 1
        }
593
594
        // get thumbnail media
595
        try {
596
            $thumbPath = $Media->getThumbnail($maxWidth, $maxHeight, $fillColor, $cropped);
597
        } catch (Exception $e) {
598
            return $this->throwNotFoundError();
599
        }
600
601
        // emit
602
        if (!headers_sent()) {
603 1
            // @codeCoverageIgnoreStart
604
            header("ETag: media-$Media->ID-$maxWidth-$maxHeight-$fillColor-$cropped");
605
            header("Content-Type: $Media->ThumbnailMIMEType");
606
            header('Content-Length: '.filesize($thumbPath));
607 1
            readfile($thumbPath);
608
            // @codeCoverageIgnoreEnd
609
        }
610
        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...
611 1
    }
612
613
614
    public function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = []): ResponseInterface
615
    {
616
        // apply tag filter
617
        if (!empty($_REQUEST['tag'])) {
618
            // get tag
619
            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...
620
                return $this->throwNotFoundError();
621
            }
622
623
            $conditions[] = 'ID IN (SELECT ContextID FROM tag_items WHERE TagID = '.$Tag->ID.' AND ContextClass = "Product")';
624
        }
625
626
627
        // apply context filter
628
        if (!empty($_REQUEST['ContextClass'])) {
629
            $conditions['ContextClass'] = $_REQUEST['ContextClass'];
630
        }
631
632
        if (!empty($_REQUEST['ContextID']) && is_numeric($_REQUEST['ContextID'])) {
633
            $conditions['ContextID'] = $_REQUEST['ContextID'];
634
        }
635
636
        return parent::handleBrowseRequest($options, $conditions, $responseID, $responseData);
637
    }
638
639
640
641
    public function handleMediaDeleteRequest(): ResponseInterface
642
    {
643
        // sanity check
644
        if (empty($_REQUEST['media']) || !is_array($_REQUEST['media'])) {
645
            return $this->throwNotFoundError();
646
        }
647
648
        // retrieve photos
649
        $media_array = [];
650
        foreach ($_REQUEST['media'] as $media_id) {
651
            if (!is_numeric($media_id)) {
652
                return $this->throwNotFoundError();
653 6
            }
654
655 6
            if ($Media = Media::getById($media_id)) {
656
                $media_array[$Media->ID] = $Media;
657
658 5
                if (!$this->checkWriteAccess($Media)) {
659
                    return $this->throwUnauthorizedError();
660 5
                }
661 5
            }
662 5
        }
663 5
664 5
        // delete
665 5
        $deleted = [];
666
        foreach ($media_array as $media_id => $Media) {
667
            if ($Media->delete()) {
668
                $deleted[] = $media_id;
669
            }
670
        }
671
672
        return $this->respond('mediaDeleted', [
673
            'success' => true
674
            ,'deleted' => $deleted,
675
        ]);
676
    }
677
678
    public function checkUploadAccess()
679
    {
680
        return true;
681
    }
682
683
    public function throwUploadError($error): ResponseInterface
684
    {
685
        return $this->respond('error', [
686
            'success' => false,
687
            'failed' => [
688
                'errors'	=>	$error,
689
            ],
690
        ]);
691
    }
692
}
693