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

MediaRequestHandler::handleBrowseRequest()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 10.5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 9
c 1
b 0
f 0
nc 9
nop 4
dl 0
loc 23
ccs 5
cts 10
cp 0.5
crap 10.5
rs 9.2222
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