Completed
Push — develop ( e92646...02469b )
by Henry
08:15
created

MediaRequestHandler::handle()   C

Complexity

Conditions 14
Paths 30

Size

Total Lines 73
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 35
c 1
b 0
f 0
nc 30
nop 1
dl 0
loc 73
rs 6.2666

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
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 $searchConditions = [
39
        'Caption' => [
40
            'qualifiers' => ['any','caption']
41
            ,'points' => 2
42
            ,'sql' => 'Caption LIKE "%%%s%%"',
43
        ]
44
        ,'CaptionLike' => [
45
            'qualifiers' => ['caption-like']
46
            ,'points' => 2
47
            ,'sql' => 'Caption LIKE "%s"',
48
        ]
49
        ,'CaptionNot' => [
50
            'qualifiers' => ['caption-not']
51
            ,'points' => 2
52
            ,'sql' => 'Caption NOT LIKE "%%%s%%"',
53
        ]
54
        ,'CaptionNotLike' => [
55
            'qualifiers' => ['caption-not-like']
56
            ,'points' => 2
57
            ,'sql' => 'Caption NOT LIKE "%s"',
58
        ],
59
    ];
60
61
    public function handle(ServerRequestInterface $request): ResponseInterface
62
    {
63
        // handle json response mode
64
        if ($this->peekPath() == 'json') {
65
            $this->shiftPath();
66
            $this->responseBuilder = JsonBuilder::class;
67
        }
68
69
        // handle action
70
        switch ($action = $this->shiftPath()) {
71
72
            case 'upload':
73
            {
74
                return $this->handleUploadRequest();
75
            }
76
77
            case 'open':
78
            {
79
                $mediaID = $this->shiftPath();
80
81
                return $this->handleMediaRequest($mediaID);
82
            }
83
84
            case 'download':
85
            {
86
                $mediaID = $this->shiftPath();
87
                $filename = urldecode($this->shiftPath());
88
89
                return $this->handleDownloadRequest($mediaID, $filename);
90
            }
91
92
            case 'info':
93
            {
94
                $mediaID = $this->shiftPath();
95
96
                return $this->handleInfoRequest($mediaID);
97
            }
98
99
            case 'caption':
100
            {
101
                $mediaID = $this->shiftPath();
102
103
                return $this->handleCaptionRequest($mediaID);
104
            }
105
106
            case 'delete':
107
            {
108
                $mediaID = $this->shiftPath();
109
                return $this->handleDeleteRequest($mediaID);
110
            }
111
112
            case 'thumbnail':
113
            {
114
                return $this->handleThumbnailRequest();
115
            }
116
117
            case false:
118
            case '':
119
            case 'browse':
120
            {
121
                if ($_SERVER['REQUEST_METHOD'] == 'POST') {
122
                    return $this->handleUploadRequest();
123
                }
124
125
                return $this->handleBrowseRequest();
126
            }
127
128
            default:
129
            {
130
                if (ctype_digit($action)) {
131
                    return $this->handleMediaRequest($action);
132
                } else {
133
                    return parent::handleRecordsRequest($action);
134
                }
135
            }
136
        }
137
    }
138
139
140
    public function handleUploadRequest($options = []): ResponseInterface
141
    {
142
        $this->checkUploadAccess();
143
144
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
145
            // init options
146
            $options = array_merge([
147
                'fieldName' => $this->uploadFileFieldName,
148
            ], $options);
149
150
151
            // check upload
152
            if (empty($_FILES[$options['fieldName']])) {
153
                return $this->throwUploadError('You did not select a file to upload');
154
            }
155
156
            // handle upload errors
157
            if ($_FILES[$options['fieldName']]['error'] != UPLOAD_ERR_OK) {
158
                switch ($_FILES[$options['fieldName']]['error']) {
159
                    case UPLOAD_ERR_NO_FILE:
160
                        return $this->throwUploadError('You did not select a file to upload');
161
162
                    case UPLOAD_ERR_INI_SIZE:
163
                    case UPLOAD_ERR_FORM_SIZE:
164
                        return $this->throwUploadError('Your file exceeds the maximum upload size. Please try again with a smaller file.');
165
166
                    case UPLOAD_ERR_PARTIAL:
167
                        return $this->throwUploadError('Your file was only partially uploaded, please try again.');
168
169
                    default:
170
                        return $this->throwUploadError('There was an unknown problem while processing your upload, please try again.');
171
                }
172
            }
173
174
            // init caption
175
            if (!isset($options['Caption'])) {
176
                if (!empty($_REQUEST['Caption'])) {
177
                    $options['Caption'] = $_REQUEST['Caption'];
178
                } else {
179
                    $options['Caption'] = preg_replace('/\.[^.]+$/', '', $_FILES[$options['fieldName']]['name']);
180
                }
181
            }
182
183
            // create media
184
            try {
185
                $Media = Media::createFromUpload($_FILES[$options['fieldName']]['tmp_name'], $options);
186
            } catch (Exception $e) {
187
                return $this->throwUploadError('The file you uploaded is not of a supported media format');
188
            }
189
        } elseif ($_SERVER['REQUEST_METHOD'] == 'PUT') {
190
            $put = fopen('php://input', 'r'); // open input stream
191
192
            $tmp = tempnam('/tmp', 'dvr');  // use PHP to make a temporary file
193
            $fp = fopen($tmp, 'w'); // open write stream to temp file
194
195
            // write
196
            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

196
            while ($data = fread(/** @scrutinizer ignore-type */ $put, 1024)) {
Loading history...
197
                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

197
                fwrite(/** @scrutinizer ignore-type */ $fp, $data);
Loading history...
198
            }
199
200
            // close handles
201
            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

201
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
202
            fclose($put);
203
204
            // create media
205
            try {
206
                $Media = Media::createFromFile($tmp, $options);
207
            } catch (Exception $e) {
208
                return $this->throwUploadError('The file you uploaded is not of a supported media format');
209
            }
210
        } else {
211
            return $this->respond('upload');
212
        }
213
214
        // assign tag
215
        /*if (!empty($_REQUEST['Tag']) && ($Tag = Tag::getByHandle($_REQUEST['Tag']))) {
216
            $Tag->assignItem('Media', $Media->ID);
217
        }*/
218
219
        // assign context
220
        if (!empty($_REQUEST['ContextClass']) && !empty($_REQUEST['ContextID'])) {
221
            if (!is_subclass_of($_REQUEST['ContextClass'], ActiveRecord::class)
222
                || !in_array($_REQUEST['ContextClass']::getStaticRootClass(), Media::$fields['ContextClass']['values'])
223
                || !is_numeric($_REQUEST['ContextID'])) {
224
                return $this->throwUploadError('Context is invalid');
225
            } elseif (!$Media->Context = $_REQUEST['ContextClass']::getByID($_REQUEST['ContextID'])) {
226
                return $this->throwUploadError('Context class not found');
227
            }
228
229
            $Media->save();
230
        }
231
232
        return $this->respond('uploadComplete', [
233
            'success' => (boolean)$Media
234
            ,'data' => $Media
235
            ,'TagID' => isset($Tag) ? $Tag->ID : null,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $Tag seems to never exist and therefore isset should always be false.
Loading history...
236
        ]);
237
    }
238
239
240
    public function handleMediaRequest($mediaID): ResponseInterface
241
    {
242
        if (empty($mediaID) || !is_numeric($mediaID)) {
243
            $this->throwError('Missing or invalid media_id');
0 ignored issues
show
Bug introduced by
The method throwError() does not exist on Divergence\Controllers\MediaRequestHandler. ( Ignorable by Annotation )

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

243
            $this->/** @scrutinizer ignore-call */ 
244
                   throwError('Missing or invalid media_id');

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

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

Loading history...
244
        }
245
246
        // get media
247
        try {
248
            $Media = Media::getById($mediaID);
249
        } catch (Exception $e) {
250
            return $this->throwUnauthorizedError();
251
        }
252
253
        if (!$Media) {
254
            $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

254
            $this->/** @scrutinizer ignore-call */ 
255
                   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...
255
        }
256
257
        if (!$this->checkReadAccess($Media)) {
0 ignored issues
show
Bug introduced by
It seems like $Media can also be of type null; however, parameter $Record of Divergence\Controllers\R...dler::checkReadAccess() does only seem to accept Divergence\Models\ActiveRecord, 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

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

334
                fseek($fp, /** @scrutinizer ignore-type */ $start);
Loading history...
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

334
                fseek(/** @scrutinizer ignore-type */ $fp, $start);
Loading history...
335
                header('HTTP/1.1 206 Partial Content');
336
            }
337
338
            // finish response
339
            header("Content-Range: bytes $start-$end/$size");
340
            header("Content-Length: $length");
341
342
            $buffer = 1024 * 8;
343
            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 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

343
            while (!feof(/** @scrutinizer ignore-type */ $fp) && ($p = ftell($fp)) <= $end) {
Loading history...
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

343
            while (!feof($fp) && ($p = ftell(/** @scrutinizer ignore-type */ $fp)) <= $end) {
Loading history...
344
                if ($p + $buffer > $end) {
345
                    $buffer = $end - $p + 1;
346
                }
347
348
                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

348
                echo fread(/** @scrutinizer ignore-type */ $fp, $buffer);
Loading history...
349
                flush();
350
            }
351
352
            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

352
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
353
        }
354
    }
355
356
    public function handleInfoRequest($mediaID): ResponseInterface
357
    {
358
        if (empty($mediaID) || !is_numeric($mediaID)) {
359
            $this->throwNotFoundError();
360
        }
361
362
        // get media
363
        try {
364
            $Media = Media::getById($mediaID);
365
        } catch (Exception $e) {
366
            return $this->throwUnauthorizedError();
367
        }
368
369
        if (!$Media) {
370
            $this->throwNotFoundError();
371
        }
372
373
        if (!$this->checkReadAccess($Media)) {
0 ignored issues
show
Bug introduced by
It seems like $Media can also be of type null; however, parameter $Record of Divergence\Controllers\R...dler::checkReadAccess() does only seem to accept Divergence\Models\ActiveRecord, 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

373
        if (!$this->checkReadAccess(/** @scrutinizer ignore-type */ $Media)) {
Loading history...
374
            return $this->throwUnauthorizedError();
375
        }
376
377
        return parent::handleRecordRequest($Media);
0 ignored issues
show
Bug introduced by
It seems like $Media can also be of type null; however, parameter $Record of Divergence\Controllers\R...::handleRecordRequest() does only seem to accept Divergence\Models\ActiveRecord, 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

377
        return parent::handleRecordRequest(/** @scrutinizer ignore-type */ $Media);
Loading history...
378
    }
379
380
    public function handleDownloadRequest($media_id, $filename = false): ResponseInterface
381
    {
382
        if (empty($media_id) || !is_numeric($media_id)) {
383
            $this->throwNotFoundError();
384
        }
385
386
        // get media
387
        try {
388
            $Media = Media::getById($media_id);
389
        } catch (Exception $e) {
390
            return $this->throwUnauthorizedError();
391
        }
392
393
394
        if (!$Media) {
395
            $this->throwNotFoundError();
396
        }
397
398
        if (!$this->checkReadAccess($Media)) {
0 ignored issues
show
Bug introduced by
It seems like $Media can also be of type null; however, parameter $Record of Divergence\Controllers\R...dler::checkReadAccess() does only seem to accept Divergence\Models\ActiveRecord, 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

398
        if (!$this->checkReadAccess(/** @scrutinizer ignore-type */ $Media)) {
Loading history...
399
            return $this->throwUnauthorizedError();
400
        }
401
402
        // determine filename
403
        if (empty($filename)) {
404
            $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

404
            $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...
405
        }
406
407
        if (strpos($filename, '.') === false) {
408
            // add extension
409
            $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...
410
        }
411
412
        header('Content-Type: '.$Media->MIMEType);
413
        header('Content-Disposition: attachment; filename="'.str_replace('"', '', $filename).'"');
414
        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

414
        header('Content-Length: '.filesize(/** @scrutinizer ignore-type */ $Media->FilesystemPath));
Loading history...
415
416
        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

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

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