Completed
Push — develop ( db4284...17cff1 )
by Henry
02:01
created

MediaRequestHandler::handleBrowseRequest()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

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
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
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
            $this->responseBuilder = JsonBuilder::class;
259
        }
260
261
        if ($this->responseBuilder == JsonBuilder::class) {
262
            return $this->respond('media', [
263
                'success' => true
264
                ,'data' => $Media,
265
            ]);
266
        } else {
267
268
            // determine variant
269
            if ($variant = $this->shiftPath()) {
270
                if (!$Media->isVariantAvailable($variant)) {
271
                    return $this->throwNotFoundError();
272
                }
273
            } else {
274
                $variant = 'original';
275
            }
276
277
            // send caching headers
278
            $expires = 60*60*24*365;
279
            if (!headers_sent()) {
280
                header("Cache-Control: public, max-age=$expires");
281
                header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time()+$expires));
282
                header('Pragma: public');
283
            }
284
285
            // media are immutable for a given URL, so no need to actually check anything if the browser wants to revalidate its cache
286
            if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
287
                header('HTTP/1.0 304 Not Modified');
288
                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...
289
            }
290
291
            // initialize response
292
            set_time_limit(0);
293
            $filePath = $Media->getFilesystemPath($variant);
294
            $fp = fopen($filePath, 'rb');
295
            $size = filesize($filePath);
296
            $length = $size;
297
            $start = 0;
298
            $end = $size - 1;
299
300
            if (!headers_sent()) {
301
                header('Content-Type: '.$Media->getMIMEType($variant));
302
                header('ETag: media-'.$Media->ID.'-'.$variant);
303
                header('Accept-Ranges: bytes');
304
            }
305
306
            // interpret range requests
307
            if (!empty($_SERVER['HTTP_RANGE'])) {
308
                $chunkStart = $start;
0 ignored issues
show
Unused Code introduced by
The assignment to $chunkStart is dead and can be removed.
Loading history...
309
                $chunkEnd = $end;
310
311
                list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
312
313
                if (strpos($range, ',') !== false) {
314
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
315
                    header("Content-Range: bytes $start-$end/$size");
316
                    exit();
317
                }
318
319
                if ($range == '-') {
320
                    $chunkStart = $size - substr($range, 1);
321
                } else {
322
                    $range = explode('-', $range);
323
                    $chunkStart = $range[0];
324
                    $chunkEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
325
                }
326
327
                $chunkEnd = ($chunkEnd > $end) ? $end : $chunkEnd;
328
                if ($chunkStart > $chunkEnd || $chunkStart > $size - 1 || $chunkEnd >= $size) {
329
                    header('HTTP/1.1 416 Requested Range Not Satisfiable');
330
                    header("Content-Range: bytes $start-$end/$size");
331
                    exit();
332
                }
333
334
                $start = $chunkStart;
335
                $end = $chunkEnd;
336
                $length = $end - $start + 1;
337
338
                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

338
                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

338
                fseek($fp, /** @scrutinizer ignore-type */ $start);
Loading history...
339
                header('HTTP/1.1 206 Partial Content');
340
            }
341
342
            // finish response
343
            if (!headers_sent()) {
344
                header("Content-Range: bytes $start-$end/$size");
345
                header("Content-Length: $length");
346
            }
347
348
            $buffer = 1024 * 8;
349
            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

349
            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

349
            while (!feof(/** @scrutinizer ignore-type */ $fp) && ($p = ftell($fp)) <= $end) {
Loading history...
350
                if ($p + $buffer > $end) {
351
                    $buffer = $end - $p + 1;
352
                }
353
354
                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

354
                echo fread(/** @scrutinizer ignore-type */ $fp, $buffer);
Loading history...
355
                flush();
356
            }
357
358
            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

358
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
359
            if ($this->responseBuilder != JsonBuilder::class) {
360
                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...
361
            }
362
        }
363
    }
364
365
    public function handleInfoRequest($mediaID): ResponseInterface
366
    {
367
        if (empty($mediaID) || !is_numeric($mediaID)) {
368
            $this->throwNotFoundError();
369
        }
370
371
        // get media
372
        try {
373
            $Media = Media::getById($mediaID);
374
        } catch (Exception $e) {
375
            return $this->throwUnauthorizedError();
376
        }
377
378
        if (!$Media) {
379
            return $this->throwNotFoundError();
380
        }
381
382
        if (!$this->checkReadAccess($Media)) {
383
            return $this->throwUnauthorizedError();
384
        }
385
386
        return parent::handleRecordRequest($Media);
387
    }
388
389
    public function handleDownloadRequest($media_id, $filename = false): ResponseInterface
390
    {
391
        if (empty($media_id) || !is_numeric($media_id)) {
392
            $this->throwNotFoundError();
393
        }
394
395
        // get media
396
        try {
397
            $Media = Media::getById($media_id);
398
        } catch (Exception $e) {
399
            return $this->throwUnauthorizedError();
400
        }
401
402
403
        if (!$Media) {
404
            return $this->throwNotFoundError();
405
        }
406
407
        if (!$this->checkReadAccess($Media)) {
408
            return $this->throwUnauthorizedError();
409
        }
410
411
        // determine filename
412
        if (empty($filename)) {
413
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, $Media->ContextID);
0 ignored issues
show
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

413
            $filename = $Media->Caption ? $Media->Caption : sprintf('%s_%u', $Media->ContextClass, /** @scrutinizer ignore-type */ $Media->ContextID);
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 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...
414
        }
415
416
        if (strpos($filename, '.') === false) {
417
            // add extension
418
            $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...
419
        }
420
421
        if (!headers_sent()) {
422
            header('Content-Type: '.$Media->MIMEType);
423
            header('Content-Disposition: attachment; filename="'.str_replace('"', '', $filename).'"');
424
            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; 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

424
            header('Content-Length: '.filesize(/** @scrutinizer ignore-type */ $Media->FilesystemPath));
Loading history...
425
        }
426
427
        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

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

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