Passed
Push — develop ( 33032a...666b96 )
by Henry
02:03
created

MediaRequestHandler::handleMediaRequest()   F

Complexity

Conditions 29
Paths 3334

Size

Total Lines 141
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 29
eloc 78
c 1
b 0
f 0
nc 3334
nop 1
dl 0
loc 141
rs 0

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

240
            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...
241
        }
242
243
        // get media
244
        try {
245
            $Media = Media::getById($mediaID);
246
        } catch (Exception $e) {
247
            return $this->throwUnauthorizedError();
248
        }
249
250
        if (!$Media) {
251
            return $this->throwNotFoundError('Media ID #%u was not found', $mediaID);
252
        }
253
254
        if (!$this->checkReadAccess($Media)) {
255
            return $this->throwNotFoundError();
256
        }
257
258
        if (isset($_SERVER['HTTP_ACCEPT'])) {
259
            if ($_SERVER['HTTP_ACCEPT'] == 'application/json') {
260
                $this->responseBuilder = JsonBuilder::class;
261
            }
262
        }
263
264
        if ($this->responseBuilder == JsonBuilder::class) {
265
            return $this->respond('media', [
266
                'success' => true
267
                ,'data' => $Media,
268
            ]);
269
        } else {
270
271
            // determine variant
272
            if ($variant = $this->shiftPath()) {
273
                if (!$Media->isVariantAvailable($variant)) {
274
                    return $this->throwNotFoundError();
275
                }
276
            } else {
277
                $variant = 'original';
278
            }
279
280
            // send caching headers
281
            $expires = 60*60*24*365;
282
            if (!headers_sent()) {
283
                // @codeCoverageIgnoreStart
284
                header("Cache-Control: public, max-age=$expires");
285
                header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time()+$expires));
286
                header('Pragma: public');
287
                // @codeCoverageIgnoreEnd
288
            }
289
290
            // media are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
291
            if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
292
                // @codeCoverageIgnoreStart
293
                header('HTTP/1.0 304 Not Modified');
294
                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...
295
                // @codeCoverageIgnoreEnd
296
            }
297
298
            // initialize response
299
            set_time_limit(0);
300
            $filePath = $Media->getFilesystemPath($variant);
301
            $fp = fopen($filePath, 'rb');
0 ignored issues
show
Bug introduced by
It seems like $filePath can also be of type null; however, parameter $filename of fopen() 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

301
            $fp = fopen(/** @scrutinizer ignore-type */ $filePath, 'rb');
Loading history...
302
            $size = filesize($filePath);
0 ignored issues
show
Bug introduced by
It seems like $filePath can also be of type null; 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

302
            $size = filesize(/** @scrutinizer ignore-type */ $filePath);
Loading history...
303
            $length = $size;
304
            $start = 0;
305
            $end = $size - 1;
306
307
            if (!headers_sent()) {
308
                // @codeCoverageIgnoreStart
309
                header('Content-Type: '.$Media->getMIMEType($variant));
310
                header('ETag: media-'.$Media->ID.'-'.$variant);
311
                header('Accept-Ranges: bytes');
312
                // @codeCoverageIgnoreEnd
313
            }
314
315
            // interpret range requests
316
            if (!empty($_SERVER['HTTP_RANGE'])) {
317
                $chunkStart = $start;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkStart is dead and can be removed.
Loading history...
318
                $chunkEnd = $end;
319
320
                list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
321
322
                if (strpos($range, ',') !== false) {
323
                    // @codeCoverageIgnoreStart
324
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
325
                    header("Content-Range: bytes $start-$end/$size");
326
                    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...
327
                    // @codeCoverageIgnoreEnd
328
                }
329
330
                if ($range == '-') {
331
                    $chunkStart = $size - substr($range, 1);
332
                } else {
333
                    $range = explode('-', $range);
334
                    $chunkStart = $range[0];
335
                    $chunkEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
336
                }
337
338
                $chunkEnd = ($chunkEnd > $end) ? $end : $chunkEnd;
339
                if ($chunkStart > $chunkEnd || $chunkStart > $size - 1 || $chunkEnd >= $size) {
340
                    // @codeCoverageIgnoreStart
341
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
342
                    header("Content-Range: bytes $start-$end/$size");
343
                    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...
344
                    // @codeCoverageIgnoreEnd
345
                }
346
347
                $start = $chunkStart;
348
                $end = $chunkEnd;
349
                $length = $end - $start + 1;
350
351
                fseek($fp, $start);
0 ignored issues
show
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

351
                fseek($fp, /** @scrutinizer ignore-type */ $start);
Loading history...
352
                // @codeCoverageIgnoreStart
353
                header('HTTP/1.1 206 Partial Content');
354
                // @codeCoverageIgnoreEnd
355
            }
356
357
            // finish response
358
            if (!headers_sent()) {
359
                // @codeCoverageIgnoreStart
360
                header("Content-Range: bytes $start-$end/$size");
361
                header("Content-Length: $length");
362
                // @codeCoverageIgnoreEnd
363
            }
364
365
            $buffer = 1024 * 8;
366
            while (!feof($fp) && ($p = ftell($fp)) <= $end) {
367
                if ($p + $buffer > $end) {
368
                    $buffer = $end - $p + 1;
369
                }
370
371
                echo fread($fp, $buffer);
372
                flush();
373
            }
374
375
            fclose($fp);
376
            if ($this->responseBuilder != JsonBuilder::class) {
377
                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...
378
            }
0 ignored issues
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 376 is false. This 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...
379
        }
380
    }
381
382
    public function handleInfoRequest($mediaID): ResponseInterface
383
    {
384
        if (empty($mediaID) || !is_numeric($mediaID)) {
385
            $this->throwNotFoundError();
386
        }
387
388
        // get media
389
        try {
390
            $Media = Media::getById($mediaID);
391
        } catch (Exception $e) {
392
            return $this->throwUnauthorizedError();
393
        }
394
395
        if (!$Media) {
396
            return $this->throwNotFoundError();
397
        }
398
399
        if (!$this->checkReadAccess($Media)) {
400
            return $this->throwUnauthorizedError();
401
        }
402
403
        return parent::handleRecordRequest($Media);
404
    }
405
406
    public function handleDownloadRequest($media_id, $filename = false): ResponseInterface
407
    {
408
        if (empty($media_id) || !is_numeric($media_id)) {
409
            $this->throwNotFoundError();
410
        }
411
412
        // get media
413
        try {
414
            $Media = Media::getById($media_id);
415
        } catch (Exception $e) {
416
            return $this->throwUnauthorizedError();
417
        }
418
419
420
        if (!$Media) {
421
            return $this->throwNotFoundError();
422
        }
423
424
        if (!$this->checkReadAccess($Media)) {
425
            return $this->throwUnauthorizedError();
426
        }
427
428
        // determine filename
429
        if (empty($filename)) {
430
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, $Media->ContextID);
0 ignored issues
show
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 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 introduced by
It seems like $Media->ContextID can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|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

430
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, /** @scrutinizer ignore-type */ $Media->ContextID);
Loading history...
431
        }
432
433
        if (strpos($filename, '.') === false) {
434
            // add extension
435
            $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...
436
        }
437
438
        if (!headers_sent()) {
439
            // @codeCoverageIgnoreStart
440
            header('Content-Type: '.$Media->MIMEType);
441
            header('Content-Disposition: attachment; filename="'.str_replace('"', '', $filename).'"');
442
            header('Content-Length: '.filesize($Media->FilesystemPath));
0 ignored issues
show
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...
Bug introduced by
It seems like $Media->FilesystemPath can also be of type array and null; 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

442
            header('Content-Length: '.filesize(/** @scrutinizer ignore-type */ $Media->FilesystemPath));
Loading history...
443
            // @codeCoverageIgnoreEnd
444
        }
445
446
        readfile($Media->FilesystemPath);
0 ignored issues
show
Bug introduced by
It seems like $Media->FilesystemPath can also be of type array and null; 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

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

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