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

MediaRequestHandler::handleUploadRequest()   D

Complexity

Conditions 20
Paths 32

Size

Total Lines 90
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 40.1955

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 52
c 1
b 0
f 0
nc 32
nop 1
dl 0
loc 90
ccs 29
cts 46
cp 0.6304
crap 40.1955
rs 4.1666

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