Passed
Push — develop ( f30815...940473 )
by Henry
03:36
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 2
    public function respondRangeNotSatisfiable(string $responseID, int $start, int $end, int $size): Response
247
    {
248 2
        $this->responseBuilder = EmptyBuilder::class;
249
250 2
        return $this->respondEmpty($responseID)
251 2
            ->withStatus(416) // Range Not Satisfiable
252 2
            ->withHeader('Content-Range', "bytes $start-$end/$size");
253
    }
254
255
    /**
256
     * Set caching headers
257
     *
258
     * @param Response $response
259
     * @return Response
260
     */
261 7
    public function setCache(Response $response): Response
262
    {
263 7
        $expires = 60*60*24*365;
264 7
        return $response->withHeader('Cache-Control', "public, max-age= $expires")
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...der('Pragma', 'public') could return the type GuzzleHttp\Psr7\MessageTrait&object which includes types incompatible with the type-hinted return Divergence\Responders\Response. Consider adding an additional type-check to rule them out.
Loading history...
265 7
        ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time()+$expires))
266 7
        ->withHeader('Pragma', 'public');
267
    }
268
269 9
    public function respondWithMedia(Media $Media, $variant, $responseID, $responseData = []): ResponseInterface
270
    {
271 9
        if ($this->responseBuilder != MediaBuilder::class) {
272
            throw new Exception('Media responses require MediaBuilder for putting together a response.');
273
        }
274 9
        $className = $this->responseBuilder;
275 9
        $responseBuilder = new $className($responseID, $responseData);
276
277 9
        $responseBuilder->setContentType($Media->MIMEType);
278
279
280 9
        $size = filesize($responseID);
281 9
        $length = $size;
282 9
        $start = 0;
283 9
        $end = $size - 1;
284
285
        // interpret range requests
286 9
        $_server = $this->request->getServerParams();
0 ignored issues
show
Bug introduced by
The method getServerParams() does not exist on null. ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

filter:
    dependency_paths: ["lib/*"]

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

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