Passed
Branch release (695ec7)
by Henry
04:31
created

MediaRequestHandler::handleDownloadRequest()   B

Complexity

Conditions 10
Paths 30

Size

Total Lines 42
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 20
c 1
b 0
f 0
nc 30
nop 2
dl 0
loc 42
rs 7.6666

How to fix   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
namespace Divergence\Controllers;
11
12
use Exception;
13
use Divergence\Helpers\JSON;
14
use Divergence\Models\Media\Media;
15
use Divergence\Models\ActiveRecord;
16
use Divergence\Responders\JsonBuilder;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\ServerRequestInterface;
19
20
class MediaRequestHandler extends RecordsRequestHandler
21
{
22
    // RecordRequestHandler configuration
23
    public static $recordClass = Media::class;
24
    public $accountLevelRead = false;
25
    public $accountLevelBrowse = 'User';
26
    public $accountLevelWrite = 'User';
27
    public $accountLevelAPI = false;
28
    public $browseLimit = 100;
29
    public $browseOrder = ['ID' => 'DESC'];
30
31
    // MediaRequestHandler configuration
32
    public $defaultPage = 'browse';
33
    public $defaultThumbnailWidth = 100;
34
    public $defaultThumbnailHeight = 100;
35
    public $uploadFileFieldName = 'mediaFile';
36
    public $responseMode = 'html';
37
38
    public static $inputStream = 'php://input'; // this is a setting so that unit tests can provide a fake stream :)
39
40
    public $searchConditions = [
41
        'Caption' => [
42
            'qualifiers' => ['any','caption']
43
            ,'points' => 2
44
            ,'sql' => 'Caption LIKE "%%%s%%"',
45
        ]
46
        ,'CaptionLike' => [
47
            'qualifiers' => ['caption-like']
48
            ,'points' => 2
49
            ,'sql' => 'Caption LIKE "%s"',
50
        ]
51
        ,'CaptionNot' => [
52
            'qualifiers' => ['caption-not']
53
            ,'points' => 2
54
            ,'sql' => 'Caption NOT LIKE "%%%s%%"',
55
        ]
56
        ,'CaptionNotLike' => [
57
            'qualifiers' => ['caption-not-like']
58
            ,'points' => 2
59
            ,'sql' => 'Caption NOT LIKE "%s"',
60
        ],
61
    ];
62
63
    public function handle(ServerRequestInterface $request): ResponseInterface
64
    {
65
        // handle json response mode
66
        if ($this->peekPath() == 'json') {
67
            $this->shiftPath();
68
            $this->responseBuilder = JsonBuilder::class;
69
        }
70
71
        // handle action
72
        switch ($action = $this->shiftPath()) {
73
74
            case 'upload':
75
            {
76
                return $this->handleUploadRequest();
77
            }
78
79
            case 'open':
80
            {
81
                $mediaID = $this->shiftPath();
82
83
                return $this->handleMediaRequest($mediaID);
84
            }
85
86
            case 'download':
87
            {
88
                $mediaID = $this->shiftPath();
89
                $filename = urldecode($this->shiftPath());
90
91
                return $this->handleDownloadRequest($mediaID, $filename);
92
            }
93
94
            case 'info':
95
            {
96
                $mediaID = $this->shiftPath();
97
98
                return $this->handleInfoRequest($mediaID);
99
            }
100
101
            case 'caption':
102
            {
103
                $mediaID = $this->shiftPath();
104
105
                return $this->handleCaptionRequest($mediaID);
106
            }
107
108
            case 'delete':
109
            {
110
                $mediaID = $this->shiftPath();
111
                return $this->handleDeleteRequest($mediaID);
112
            }
113
114
            case 'thumbnail':
115
            {
116
                return $this->handleThumbnailRequest();
117
            }
118
119
            case false:
120
            case '':
121
            case 'browse':
122
            {
123
                if ($_SERVER['REQUEST_METHOD'] == 'POST') {
124
                    return $this->handleUploadRequest();
125
                }
126
127
                return $this->handleBrowseRequest();
128
            }
129
130
            default:
131
            {
132
                if (ctype_digit($action)) {
133
                    return $this->handleMediaRequest($action);
134
                } else {
135
                    return parent::handleRecordsRequest($action);
136
                }
137
            }
138
        }
139
    }
140
141
142
    public function handleUploadRequest($options = []): ResponseInterface
143
    {
144
        $this->checkUploadAccess();
145
146
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
147
            // init options
148
            $options = array_merge([
149
                'fieldName' => $this->uploadFileFieldName,
150
            ], $options);
151
152
153
            // check upload
154
            if (empty($_FILES[$options['fieldName']])) {
155
                return $this->throwUploadError('You did not select a file to upload');
156
            }
157
158
            // handle upload errors
159
            if ($_FILES[$options['fieldName']]['error'] != UPLOAD_ERR_OK) {
160
                switch ($_FILES[$options['fieldName']]['error']) {
161
                    case UPLOAD_ERR_NO_FILE:
162
                        return $this->throwUploadError('You did not select a file to upload');
163
164
                    case UPLOAD_ERR_INI_SIZE:
165
                    case UPLOAD_ERR_FORM_SIZE:
166
                        return $this->throwUploadError('Your file exceeds the maximum upload size. Please try again with a smaller file.');
167
168
                    case UPLOAD_ERR_PARTIAL:
169
                        return $this->throwUploadError('Your file was only partially uploaded, please try again.');
170
171
                    default:
172
                        return $this->throwUploadError('There was an unknown problem while processing your upload, please try again.');
173
                }
174
            }
175
176
            // init caption
177
            if (!isset($options['Caption'])) {
178
                if (!empty($_REQUEST['Caption'])) {
179
                    $options['Caption'] = $_REQUEST['Caption'];
180
                } else {
181
                    $options['Caption'] = preg_replace('/\.[^.]+$/', '', $_FILES[$options['fieldName']]['name']);
182
                }
183
            }
184
185
            // create media
186
            try {
187
                $Media = Media::createFromUpload($_FILES[$options['fieldName']]['tmp_name'], $options);
188
            } catch (Exception $e) {
189
                return $this->throwUploadError($e->getMessage());
190
            }
191
        } elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
192
            $put = fopen(static::$inputStream, 'r'); // open input stream
193
194
            $tmp = tempnam('/tmp', 'dvr');  // use PHP to make a temporary file
195
            $fp = fopen($tmp, 'w'); // open write stream to temp file
196
197
            // write
198
            while ($data = fread($put, 1024)) {
0 ignored issues
show
Bug introduced by
It seems like $put can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

198
            while ($data = fread(/** @scrutinizer ignore-type */ $put, 1024)) {
Loading history...
199
                fwrite($fp, $data);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, 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

199
                fwrite(/** @scrutinizer ignore-type */ $fp, $data);
Loading history...
200
            }
201
202
            // close handles
203
            fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

203
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
204
            fclose($put);
205
206
            // create media
207
            try {
208
                $Media = Media::createFromFile($tmp, $options);
209
            } catch (Exception $e) {
210
                return $this->throwUploadError('The file you uploaded is not of a supported media format');
211
            }
212
        } else {
213
            return $this->respond('upload');
214
        }
215
216
        // assign context
217
        if (!empty($_REQUEST['ContextClass']) && !empty($_REQUEST['ContextID'])) {
218
            if (!is_subclass_of($_REQUEST['ContextClass'], ActiveRecord::class)
219
                || !in_array($_REQUEST['ContextClass']::getStaticRootClass(), Media::$fields['ContextClass']['values'])
220
                || !is_numeric($_REQUEST['ContextID'])) {
221
                return $this->throwUploadError('Context is invalid');
222
            } elseif (!$Media->Context = $_REQUEST['ContextClass']::getByID($_REQUEST['ContextID'])) {
223
                return $this->throwUploadError('Context class not found');
224
            }
225
226
            $Media->save();
227
        }
228
229
        return $this->respond('uploadComplete', [
230
            'success' => (boolean)$Media
231
            ,'data' => $Media,
232
        ]);
233
    }
234
235
236
    public function handleMediaRequest($mediaID): ResponseInterface
237
    {
238
        if (empty($mediaID) || !is_numeric($mediaID)) {
239
            return $this->throwNotFoundError('Media ID #%u was not found', $mediaID);
0 ignored issues
show
Unused Code introduced by
The call to Divergence\Controllers\R...r::throwNotFoundError() has too many arguments starting with 'Media ID #%u was not found'. ( Ignorable by Annotation )

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

239
            return $this->/** @scrutinizer ignore-call */ throwNotFoundError('Media ID #%u was not found', $mediaID);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
240
        }
241
242
        // get media
243
        try {
244
            $Media = Media::getById($mediaID);
245
        } catch (Exception $e) {
246
            return $this->throwUnauthorizedError();
247
        }
248
249
        if (!$Media) {
250
            return $this->throwNotFoundError('Media ID #%u was not found', $mediaID);
251
        }
252
253
        if (!$this->checkReadAccess($Media)) {
254
            return $this->throwNotFoundError();
255
        }
256
257
        if (isset($_SERVER['HTTP_ACCEPT'])) {
258
            if ($_SERVER['HTTP_ACCEPT'] == 'application/json') {
259
                $this->responseBuilder = JsonBuilder::class;
260
            }
261
        }
262
263
        if ($this->responseBuilder == JsonBuilder::class) {
264
            return $this->respond('media', [
265
                'success' => true
266
                ,'data' => $Media,
267
            ]);
268
        } else {
269
270
            // determine variant
271
            if ($variant = $this->shiftPath()) {
272
                if (!$Media->isVariantAvailable($variant)) {
273
                    return $this->throwNotFoundError();
274
                }
275
            } else {
276
                $variant = 'original';
277
            }
278
279
            // send caching headers
280
            $expires = 60*60*24*365;
281
            if (!headers_sent()) {
282
                // @codeCoverageIgnoreStart
283
                header("Cache-Control: public, max-age=$expires");
284
                header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time()+$expires));
285
                header('Pragma: public');
286
                // @codeCoverageIgnoreEnd
287
            }
288
289
            // media are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
290
            if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
291
                // @codeCoverageIgnoreStart
292
                header('HTTP/1.0 304 Not Modified');
293
                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...
294
                // @codeCoverageIgnoreEnd
295
            }
296
297
            // initialize response
298
            set_time_limit(0);
299
            $filePath = $Media->getFilesystemPath($variant);
300
            $fp = fopen($filePath, 'rb');
301
            $size = filesize($filePath);
302
            $length = $size;
303
            $start = 0;
304
            $end = $size - 1;
305
306
            if (!headers_sent()) {
307
                // @codeCoverageIgnoreStart
308
                header('Content-Type: '.$Media->getMIMEType($variant));
309
                header('ETag: media-'.$Media->ID.'-'.$variant);
310
                header('Accept-Ranges: bytes');
311
                // @codeCoverageIgnoreEnd
312
            }
313
314
            // interpret range requests
315
            if (!empty($_SERVER['HTTP_RANGE'])) {
316
                $chunkStart = $start;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkStart is dead and can be removed.
Loading history...
317
                $chunkEnd = $end;
318
319
                list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
320
321
                if (strpos($range, ',') !== false) {
322
                    // @codeCoverageIgnoreStart
323
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
324
                    header("Content-Range: bytes $start-$end/$size");
325
                    exit();
326
                    // @codeCoverageIgnoreEnd
327
                }
328
329
                if ($range == '-') {
330
                    $chunkStart = $size - substr($range, 1);
331
                } else {
332
                    $range = explode('-', $range);
333
                    $chunkStart = $range[0];
334
                    $chunkEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
335
                }
336
337
                $chunkEnd = ($chunkEnd > $end) ? $end : $chunkEnd;
338
                if ($chunkStart > $chunkEnd || $chunkStart > $size - 1 || $chunkEnd >= $size) {
339
                    // @codeCoverageIgnoreStart
340
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
341
                    header("Content-Range: bytes $start-$end/$size");
342
                    exit();
343
                    // @codeCoverageIgnoreEnd
344
                }
345
346
                $start = $chunkStart;
347
                $end = $chunkEnd;
348
                $length = $end - $start + 1;
349
350
                fseek($fp, $start);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fseek() does only seem to accept resource, 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

350
                fseek(/** @scrutinizer ignore-type */ $fp, $start);
Loading history...
Bug introduced by
It seems like $start can also be of type string; however, parameter $offset of fseek() does only seem to accept integer, 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

350
                fseek($fp, /** @scrutinizer ignore-type */ $start);
Loading history...
351
                // @codeCoverageIgnoreStart
352
                header('HTTP/1.1 206 Partial Content');
353
                // @codeCoverageIgnoreEnd
354
            }
355
356
            // finish response
357
            if (!headers_sent()) {
358
                // @codeCoverageIgnoreStart
359
                header("Content-Range: bytes $start-$end/$size");
360
                header("Content-Length: $length");
361
                // @codeCoverageIgnoreEnd
362
            }
363
364
            $buffer = 1024 * 8;
365
            while (!feof($fp) && ($p = ftell($fp)) <= $end) {
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of ftell() does only seem to accept resource, 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

365
            while (!feof($fp) && ($p = ftell(/** @scrutinizer ignore-type */ $fp)) <= $end) {
Loading history...
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of feof() does only seem to accept resource, 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

365
            while (!feof(/** @scrutinizer ignore-type */ $fp) && ($p = ftell($fp)) <= $end) {
Loading history...
366
                if ($p + $buffer > $end) {
367
                    $buffer = $end - $p + 1;
368
                }
369
370
                echo fread($fp, $buffer);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

370
                echo fread(/** @scrutinizer ignore-type */ $fp, $buffer);
Loading history...
371
                flush();
372
            }
373
374
            fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

374
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
375
            if ($this->responseBuilder != JsonBuilder::class) {
376
                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...
377
            }
378
        }
379
    }
380
381
    public function handleInfoRequest($mediaID): ResponseInterface
382
    {
383
        if (empty($mediaID) || !is_numeric($mediaID)) {
384
            $this->throwNotFoundError();
385
        }
386
387
        // get media
388
        try {
389
            $Media = Media::getById($mediaID);
390
        } catch (Exception $e) {
391
            return $this->throwUnauthorizedError();
392
        }
393
394
        if (!$Media) {
395
            return $this->throwNotFoundError();
396
        }
397
398
        if (!$this->checkReadAccess($Media)) {
399
            return $this->throwUnauthorizedError();
400
        }
401
402
        return parent::handleRecordRequest($Media);
403
    }
404
405
    public function handleDownloadRequest($media_id, $filename = false): ResponseInterface
406
    {
407
        if (empty($media_id) || !is_numeric($media_id)) {
408
            $this->throwNotFoundError();
409
        }
410
411
        // get media
412
        try {
413
            $Media = Media::getById($media_id);
414
        } catch (Exception $e) {
415
            return $this->throwUnauthorizedError();
416
        }
417
418
419
        if (!$Media) {
420
            return $this->throwNotFoundError();
421
        }
422
423
        if (!$this->checkReadAccess($Media)) {
424
            return $this->throwUnauthorizedError();
425
        }
426
427
        // determine filename
428
        if (empty($filename)) {
429
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, $Media->ContextID);
0 ignored issues
show
Bug Best Practice introduced by
The property ContextID does not exist on Divergence\Models\Media\Media. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property Caption does not exist on Divergence\Models\Media\Media. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like $Media->ContextID can also be of type array; however, parameter $args of sprintf() 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

429
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, /** @scrutinizer ignore-type */ $Media->ContextID);
Loading history...
430
        }
431
432
        if (strpos($filename, '.') === false) {
433
            // add extension
434
            $filename .= '.'.$Media->Extension;
0 ignored issues
show
Bug Best Practice introduced by
The property Extension does not exist on Divergence\Models\Media\Media. Since you implemented __get, consider adding a @property annotation.
Loading history...
435
        }
436
437
        if (!headers_sent()) {
438
            // @codeCoverageIgnoreStart
439
            header('Content-Type: '.$Media->MIMEType);
440
            header('Content-Disposition: attachment; filename="'.str_replace('"', '', $filename).'"');
441
            header('Content-Length: '.filesize($Media->FilesystemPath));
0 ignored issues
show
Bug introduced by
It seems like $Media->FilesystemPath can also be of type array; however, parameter $filename of filesize() 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

441
            header('Content-Length: '.filesize(/** @scrutinizer ignore-type */ $Media->FilesystemPath));
Loading history...
Bug Best Practice introduced by
The property FilesystemPath does not exist on Divergence\Models\Media\Media. Since you implemented __get, consider adding a @property annotation.
Loading history...
442
            // @codeCoverageIgnoreEnd
443
        }
444
445
        readfile($Media->FilesystemPath);
0 ignored issues
show
Bug introduced by
It seems like $Media->FilesystemPath can also be of type array; however, parameter $filename of readfile() 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

445
        readfile(/** @scrutinizer ignore-type */ $Media->FilesystemPath);
Loading history...
446
        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...
447
    }
448
449
    public function handleCaptionRequest($media_id): ResponseInterface
450
    {
451
        // require authentication
452
        $GLOBALS['Session']->requireAccountLevel('Staff');
453
454
        if (empty($media_id) || !is_numeric($media_id)) {
455
            return $this->throwNotFoundError();
456
        }
457
458
        // get media
459
        try {
460
            $Media = Media::getById($media_id);
461
        } catch (Exception $e) {
462
            return $this->throwUnauthorizedError();
463
        }
464
465
466
        if (!$Media) {
467
            $this->throwNotFoundError();
468
        }
469
470
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
471
            $Media->Caption = $_REQUEST['Caption'];
0 ignored issues
show
Bug Best Practice introduced by
The property Caption does not exist on Divergence\Models\Media\Media. Since you implemented __set, consider adding a @property annotation.
Loading history...
472
            $Media->save();
473
474
            return $this->respond('mediaCaptioned', [
475
                'success' => true
476
                ,'data' => $Media,
477
            ]);
478
        }
479
480
        return $this->respond('mediaCaption', [
481
            'data' => $Media,
482
        ]);
483
    }
484
485
    public function handleDeleteRequest(ActiveRecord $Record): ResponseInterface
486
    {
487
        // require authentication
488
        $GLOBALS['Session']->requireAccountLevel('Staff');
489
490
        if ($mediaID = $this->peekPath()) {
491
            $mediaIDs = [$mediaID];
492
        } elseif (!empty($_REQUEST['mediaID'])) {
493
            $mediaIDs = [$_REQUEST['mediaID']];
494
        } elseif (is_array($_REQUEST['media'])) {
495
            $mediaIDs = $_REQUEST['media'];
496
        }
497
498
        $deleted = [];
499
        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...
500
            if (!is_numeric($mediaID)) {
501
                continue;
502
            }
503
504
            // get media
505
            $Media = Media::getByID($mediaID);
506
507
            if (!$Media) {
508
                return $this->throwNotFoundError();
509
            }
510
511
            if ($Media->destroy()) {
512
                $deleted[] = $Media;
513
            }
514
        }
515
516
        return $this->respond('mediaDeleted', [
517
            'success' => true
518
            ,'data' => $deleted,
519
        ]);
520
    }
521
522
    public function handleThumbnailRequest(Media $Media = null): ResponseInterface
523
    {
524
        // send caching headers
525
        if (!headers_sent()) {
526
            // @codeCoverageIgnoreStart
527
            $expires = 60*60*24*365;
528
            header("Cache-Control: public, max-age=$expires");
529
            header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time()+$expires));
530
            header('Pragma: public');
531
            // @codeCoverageIgnoreEnd
532
        }
533
534
        // thumbnails are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
535
        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
536
            header('HTTP/1.0 304 Not Modified');
537
            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...
538
        }
539
540
        // get media
541
        if (!$Media) {
542
            if (!$mediaID = $this->shiftPath()) {
543
                return $this->throwNotFoundError();
544
            } elseif (!$Media = Media::getByID($mediaID)) {
545
                return $this->throwNotFoundError();
546
            }
547
        }
548
549
        // get format
550
        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

550
        if (preg_match('/^(\d+)x(\d+)(x([0-9A-F]{6})?)?$/i', /** @scrutinizer ignore-type */ $this->peekPath(), $matches)) {
Loading history...
551
            $this->shiftPath();
552
            $maxWidth = $matches[1];
553
            $maxHeight = $matches[2];
554
            $fillColor = !empty($matches[4]) ? $matches[4] : false;
555
        } else {
556
            $maxWidth = $this->defaultThumbnailWidth;
557
            $maxHeight = $this->defaultThumbnailHeight;
558
            $fillColor = false;
559
        }
560
561
        if ($this->peekPath() == 'cropped') {
562
            $this->shiftPath();
563
            $cropped = true;
564
        } else {
565
            $cropped = false;
566
        }
567
568
        // get thumbnail media
569
        try {
570
            $thumbPath = $Media->getThumbnail($maxWidth, $maxHeight, $fillColor, $cropped);
571
        } catch (Exception $e) {
572
            return $this->throwNotFoundError();
573
        }
574
575
        // emit
576
        if (!headers_sent()) {
577
            // @codeCoverageIgnoreStart
578
            header("ETag: media-$Media->ID-$maxWidth-$maxHeight-$fillColor-$cropped");
579
            header("Content-Type: $Media->ThumbnailMIMEType");
0 ignored issues
show
Bug Best Practice introduced by
The property ThumbnailMIMEType does not exist on Divergence\Models\Media\Media. Since you implemented __get, consider adding a @property annotation.
Loading history...
580
            header('Content-Length: '.filesize($thumbPath));
581
            readfile($thumbPath);
582
            // @codeCoverageIgnoreEnd
583
        }
584
        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...
585
    }
586
587
588
    public function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = []): ResponseInterface
589
    {
590
        // apply tag filter
591
        if (!empty($_REQUEST['tag'])) {
592
            // get tag
593
            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...
594
                return $this->throwNotFoundError();
595
            }
596
597
            $conditions[] = 'ID IN (SELECT ContextID FROM tag_items WHERE TagID = '.$Tag->ID.' AND ContextClass = "Product")';
598
        }
599
600
601
        // apply context filter
602
        if (!empty($_REQUEST['ContextClass'])) {
603
            $conditions['ContextClass'] = $_REQUEST['ContextClass'];
604
        }
605
606
        if (!empty($_REQUEST['ContextID']) && is_numeric($_REQUEST['ContextID'])) {
607
            $conditions['ContextID'] = $_REQUEST['ContextID'];
608
        }
609
610
        return parent::handleBrowseRequest($options, $conditions, $responseID, $responseData);
611
    }
612
613
614
615
    public function handleMediaDeleteRequest(): ResponseInterface
616
    {
617
        // sanity check
618
        if (empty($_REQUEST['media']) || !is_array($_REQUEST['media'])) {
619
            return $this->throwNotFoundError();
620
        }
621
622
        // retrieve photos
623
        $media_array = [];
624
        foreach ($_REQUEST['media'] as $media_id) {
625
            if (!is_numeric($media_id)) {
626
                return $this->throwNotFoundError();
627
            }
628
629
            if ($Media = Media::getById($media_id)) {
630
                $media_array[$Media->ID] = $Media;
631
632
                if (!$this->checkWriteAccess($Media)) {
633
                    return $this->throwUnauthorizedError();
634
                }
635
            }
636
        }
637
638
        // delete
639
        $deleted = [];
640
        foreach ($media_array as $media_id => $Media) {
641
            if ($Media->delete()) {
642
                $deleted[] = $media_id;
643
            }
644
        }
645
646
        return $this->respond('mediaDeleted', [
647
            'success' => true
648
            ,'deleted' => $deleted,
649
        ]);
650
    }
651
652
    public function checkUploadAccess()
653
    {
654
        return true;
655
    }
656
657
    public function throwUploadError($error): ResponseInterface
658
    {
659
        return $this->respond('error', [
660
            'success' => false,
661
            'failed' => [
662
                'errors'	=>	$error,
663
            ],
664
        ]);
665
    }
666
}
667