Passed
Push — develop ( 16acaa...f30815 )
by Henry
03:55 queued 43s
created

MediaRequestHandler::handleMediaRequest()   C

Complexity

Conditions 12
Paths 22

Size

Total Lines 62
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 14.25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 33
c 1
b 0
f 0
nc 22
nop 1
dl 0
loc 62
ccs 24
cts 32
cp 0.75
crap 14.25
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
            ,'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
    /**
247
     * Set caching headers
248
     *
249
     * @param Response $response
250
     * @return Response
251
     */
252 7
    public function setCache(Response $response): Response
253
    {
254 7
        $expires = 60*60*24*365;
255 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...
256 7
        ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time()+$expires))
257 7
        ->withHeader('Pragma', 'public');
258
    }
259
260 9
    public function respondWithMedia(Media $Media, $variant, $responseID, $responseData = []): ResponseInterface
261
    {
262 9
        if ($this->responseBuilder != MediaBuilder::class) {
263
            throw new Exception('Media responses require MediaBuilder for putting together a response.');
264
        }
265 9
        $className = $this->responseBuilder;
266 9
        $responseBuilder = new $className($responseID, $responseData);
267
268 9
        $responseBuilder->setContentType($Media->MIMEType);
269
270
271 9
        $size = filesize($responseID);
272 9
        $length = $size;
273 9
        $start = 0;
274 9
        $end = $size - 1;
275
276
        // interpret range requests
277 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

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

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

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