Passed
Branch release (e4b72f)
by Henry
02:55
created

Media::getValue()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 40.5409

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
c 1
b 0
f 0
nc 11
nop 1
dl 0
loc 49
ccs 12
cts 32
cp 0.375
crap 40.5409
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Models\Media;
12
13
use Exception;
14
use Divergence\App;
15
use Divergence\Models\Model;
16
use Divergence\Models\Mapping\Column;
17
18
/**
19
 * Media Model
20
 *
21
 * @author Henry Paradiz <[email protected]>
22
 * @author  Chris Alfano <[email protected]>
23
 *
24
 * {@inheritDoc}
25
 * @property int        $CreatorID A standard user ID field for use by your login & authentication system. Part of Divergence\Models\Model but used in this file as a default.
26
 *
27
 * @property-read array $Data           JSON object about the media
28
 * @property-read array $SummaryData    JSON object about the media
29
 * @property-read array $JsonTranslation  JSON object about the media
30
 *
31
 * @property-read string $Filename Filename without path but appended
32
 * @property-read string $ThumbnailMIMEType The mimetype for the thumbnail
33
 * @property-read string $WebPath      Uses static::$webPathFormat to generate a URL path for you.
34
 * @property-read string $getFilesystemPath Full filesystem path
35
 *
36
 * @method static void _defineRelationships()
37
 * @method static void _initRelationships()
38
 */
39
class Media extends Model
40
{
41
    public static $useCache = true;
42
    public static $singularNoun = 'media item';
43
    public static $pluralNoun = 'media items';
44
45
    // support subclassing
46
    public static $rootClass = __CLASS__;
47
    public static $defaultClass = __CLASS__;
48
    public static $subClasses = [__CLASS__, Image::class, PDF::class, Video::class, Audio::class];
49
    public static $collectionRoute = '/media';
50
51
    public static $tableName = 'media';
52
53
    #[Column(notnull: false, default:null)]
54
    protected $ContextClass;
55
56
    #[Column(type:'int', notnull: false, default:null)]
57
    protected $ContextID;
58
59
    protected $MIMEType;
60
61
    #[Column(type:'int', unsigned: true, notnull:false)]
62
    protected $Width;
63
64
    #[Column(type:'int', unsigned: true, notnull:false)]
65
    protected $Height;
66
67
    #[Column(type: 'decimal', notnull: false, precision: 12, scale: 6, default: 0)]
68
    protected $Duration;
69
70
    #[Column(notnull:false)]
71
    protected $Caption;
72
73
74
    public static $relationships = [
75
        'Creator' => [
76
            'type' => 'one-one',
77
            'class' => 'Person',
78
            'local' => 'CreatorID',
79
        ],
80
        'Context' => [
81
            'type' => 'context-parent',
82
        ],
83
    ];
84
85
    public static $searchConditions = [
86
        'Caption' => [
87
            'qualifiers' => ['any','caption'],
88
            'points' => 2,
89
            'sql' => 'Caption LIKE "%%%s%%"',
90
        ],
91
        'CaptionLike' => [
92
            'qualifiers' => ['caption-like'],
93
            'points' => 2,
94
            'sql' => 'Caption LIKE "%s"',
95
        ],
96
        'CaptionNot' => [
97
            'qualifiers' => ['caption-not'],
98
            'points' => 2,
99
            'sql' => 'Caption NOT LIKE "%%%s%%"',
100
        ],
101
        'CaptionNotLike' => [
102
            'qualifiers' => ['caption-not-like'],
103
            'points' => 2,
104
            'sql' => 'Caption NOT LIKE "%s"',
105
        ],
106
    ];
107
108
    public static $webPathFormat = '/media/open/%u'; // 1=mediaID
109
    public static $thumbnailRequestFormat = '/thumbnail/%1$u/%2$ux%3$u%4$s'; // 1=media_id 2=width 3=height 4=fill_color
110
    #public static $blankThumbnailRequestFormat = '/thumbnail/%1$s/%2$ux%3$u%4$s'; // 1=class 2=width 3=height 4=fill_color
111
    public static $thumbnailJPEGCompression = 90;
112
    public static $thumbnailPNGCompression = 9;
113
    public static $defaultFilenameFormat = 'default.%s.jpg';
114
    public static $newDirectoryPermissions = 0775;
115
    public static $newFilePermissions = 0664;
116
    public static $magicPath = null;//'/usr/share/misc/magic.mgc';
117
    public static $useFaceDetection = true;
118
    public static $faceDetectionTimeLimit = 10;
119
120
    public static $mimeHandlers = [
121
        'image/gif' => Image::class,
122
        'image/jpeg' => Image::class,
123
        'image/png' => Image::class,
124
        'image/tiff' => Image::class,
125
        'application/psd' => Image::class,
126
        'audio/mpeg' => Audio::class,
127
        'application/pdf' => PDF::class,
128
        'application/postscript' => PDF::class,
129
        'image/svg+xml' => PDF::class,
130
        'video/x-flv' => Video::class,
131
        'video/mp4' => Video::class,
132
        'video/quicktime' => Video::class,
133
    ];
134
135
    public static $mimeRewrites = [
136
        'image/photoshop'              => 'application/psd',
137
        'image/x-photoshop'            => 'application/psd',
138
        'image/psd'                    => 'application/psd',
139
        'application/photoshop'        => 'application/psd',
140
        'image/vnd.adobe.photoshop'    => 'application/psd',
141
    ];
142
143 19
    public function getValue($name)
144
    {
145
        switch ($name) {
146 19
            case 'Data':
147 19
            case 'SummaryData':
148 19
            case 'JsonTranslation':
149
                return [
150
                    'ID' => $this->getValue('ID'),
151
                    'Class' => $this->getValue('Class'),
152
                    'ContextClass' => $this->getValue('ContextClass'),
153
                    'ContextID' => $this->getValue('ContextID'),
154
                    'MIMEType' => $this->getValue('MIMEType'),
155
                    'Width' => $this->getValue('Width'),
156
                    'Height' => $this->getValue('Height'),
157
                    'Duration' => $this->getValue('Duration'),
158
                ];
159
160 19
            case 'Filename':
161 3
                return $this->getFilename();
162
163 19
            case 'ThumbnailMIMEType':
164
                return $this->getValue('MIMEType');
165
166 19
            case 'Extension':
167
                throw new Exception('Unable to find extension for mime-type: '.$this->getValue('MIMEType'));
168
169 19
            case 'WebPath':
170
                return sprintf(
171
                    static::$webPathFormat,
172
                    $this->getValue('ID')
173
                );
174
175 19
            case 'FilesystemPath':
176 6
                return $this->getFilesystemPath();
177
178
179
            default:
180 19
                return parent::getValue($name);
181
        }
182
    }
183
184
    /**
185
     * Builds a string for the correct path to the resource with all the options provided
186
     *
187
     * @param int|string $width
188
     * @param int|string $height
189
     * @param int|string $fillColor
190
     * @param bool $cropped
191
     * @return string
192
     * @throws Exception
193
     */
194
    public function buildThumbnailRequest($width, $height = null, $fillColor = null, $cropped = false)
195
    {
196
        return sprintf(
197
            static::$thumbnailRequestFormat,
198
            $this->getValue('ID'),
199
            $width,
200
            $height ?: $width,
201
            (is_string($fillColor) ? 'x'.$fillColor : '')
202
        ).($cropped ? '/cropped' : '');
203
    }
204
205
    /**
206
     * Creates a GdImage object from a file
207
     *
208
     *  Supports psd, tiff, pdf, postscript, and anything else that gd supports natively
209
     *
210
     * @param ?string $sourceFile If not provided will try to figure it out from $this->FilesystemPath
211
     * @throws Exception
212
     * @return \GdImage|false
213
     */
214 3
    public function getImage($sourceFile = null): \GdImage|false
215
    {
216 3
        if (!isset($sourceFile)) {
217 3
            $sourceFile = $this->getValue('FilesystemPath') ? $this->getValue('FilesystemPath') : $this->getValue('BlankPath');
218
        }
219
220 3
        switch ($this->getValue('MIMEType')) {
221 3
            case 'application/psd':
222 3
            case 'image/tiff':
223
224
                //Converts PSD to PNG temporarily on the real file system.
225
                $tempFile = tempnam('/tmp', 'media_convert');
226
                exec("convert -density 100 ".$this->getValue('FilesystemPath')."[0] -flatten $tempFile.png");
227
228
                return imagecreatefrompng("$tempFile.png");
229
230 3
            case 'application/postscript':
231
232
                return imagecreatefromstring(shell_exec("gs -r150 -dEPSCrop -dNOPAUSE -dBATCH -sDEVICE=png48 -sOutputFile=- -q $this->getValue('FilesystemPath')"));
233
234
            default:
235
236 3
                if (!$fileData = file_get_contents($sourceFile)) {
237
                    throw new Exception('Could not load media source: '.$sourceFile);
238
                }
239
240 3
                $image = imagecreatefromstring($fileData);
241
242 3
                if ($this->getValue('MIMEType') == 'image/jpeg' && ($exifData = exif_read_data($sourceFile)) && !empty($exifData['Orientation'])) {
243
                    switch ($exifData['Orientation']) {
244
                        case 1: // nothing
245
                            break;
246
                        case 2: // horizontal flip
247
                            imageflip($image, IMG_FLIP_HORIZONTAL);
248
                            break;
249
                        case 3: // 180 rotate left
250
                            $image = imagerotate($image, 180, 0);
251
                            break;
252
                        case 4: // vertical flip
253
                            imageflip($image, IMG_FLIP_VERTICAL);
254
                            break;
255
                        case 5: // vertical flip + 90 rotate right
256
                            imageflip($image, IMG_FLIP_VERTICAL);
257
                            $image = imagerotate($image, -90, 0);
258
                            break;
259
                        case 6: // 90 rotate right
260
                            $image = imagerotate($image, -90, 0);
261
                            break;
262
                        case 7: // horizontal flip + 90 rotate right
263
                            imageflip($image, IMG_FLIP_HORIZONTAL);
264
                            $image = imagerotate($image, -90, 0);
265
                            break;
266
                        case 8: // 90 rotate left
267
                            $image = imagerotate($image, 90, 0);
268
                            break;
269
                    }
270
                }
271
272 3
                return $image;
273
        }
274
    }
275
276
    /**
277
     * Gives us the path to a thumbnail and if it doesn't exist creates it
278
     *
279
     * @param int $maxWidth
280
     * @param int $maxHeight
281
     * @param string|bool $fillColor
282
     * @param boolean $cropped
283
     * @return string
284
     */
285 3
    public function getThumbnail($maxWidth, $maxHeight, $fillColor = false, $cropped = false)
286
    {
287 3
        $thumbFormat = sprintf('%ux%u', $maxWidth, $maxHeight);
288
289 3
        if ($fillColor) {
290
            $thumbFormat .= 'x'.strtoupper($fillColor);
291
        }
292
293 3
        if ($cropped) {
294
            $thumbFormat .= '.cropped';
295
        }
296
297 3
        $thumbPath = App::$App->ApplicationPath.'/media/'.$thumbFormat.'/'.$this->getValue('Filename');
298
299
        // look for cached thumbnail
300 3
        if (!file_exists($thumbPath)) {
301
            // ensure directory exists
302 3
            $thumbDir = dirname($thumbPath);
303 3
            if (!is_dir($thumbDir)) {
304 3
                mkdir($thumbDir, static::$newDirectoryPermissions, true);
305
            }
306
307
            // create new thumbnail
308 3
            $this->createThumbnailImage($thumbPath, $maxWidth, $maxHeight, $fillColor, $cropped);
309
        }
310
311 3
        return $thumbPath;
312
    }
313
314
    /**
315
     * Creates a thumbnail using the gd php extension.
316
     * Keeps aspect ratio.
317
     *
318
     * @param string $thumbPath
319
     * @param int $maxWidth
320
     * @param int $maxHeight
321
     * @param boolean|int $fillColor Background canvas color. See gd documentation.
322
     * @param boolean $cropped If cropped is enable the image will instead fill the smaller of width or height and cut the edges off.
323
     * @return void
324
     */
325 3
    public function createThumbnailImage($thumbPath, $maxWidth, $maxHeight, $fillColor = false, $cropped = false): void
326
    {
327 3
        $thumbWidth = $maxWidth;
328 3
        $thumbHeight = $maxHeight;
329
330
        // load source image
331 3
        $srcImage = $this->getImage();
332 3
        if (!$srcImage) {
333
            throw new Exception('This type of media file can not create a thumbnail image.');
334
        }
335 3
        $srcWidth = imagesx($srcImage);
336 3
        $srcHeight = imagesy($srcImage);
337
338
        // calculate
339 3
        if ($srcWidth && $srcHeight) {
340 3
            $widthRatio = ($srcWidth > $maxWidth) ? ($maxWidth / $srcWidth) : 1;
341 3
            $heightRatio = ($srcHeight > $maxHeight) ? ($maxHeight / $srcHeight) : 1;
342
343
            // crop width/height to scale size if fill disabled
344 3
            if ($cropped) {
345
                $ratio = max($widthRatio, $heightRatio);
346
            } else {
347 3
                $ratio = min($widthRatio, $heightRatio);
348
            }
349
350 3
            $scaledWidth = round($srcWidth * $ratio);
351 3
            $scaledHeight = round($srcHeight * $ratio);
352
        } else {
353
            $scaledWidth = $maxWidth;
354
            $scaledHeight = $maxHeight;
355
        }
356
357 3
        if (!$fillColor && !$cropped) {
358 3
            $thumbWidth = $scaledWidth;
359 3
            $thumbHeight = $scaledHeight;
360
        }
361
362
        // create thumbnail images
363 3
        $image = imagecreatetruecolor($thumbWidth, $thumbHeight);
364
365
        // paint fill color
366 3
        if ($fillColor) {
367
            // extract decimal values from hex triplet
368
            $fillColor = sscanf($fillColor, '%2x%2x%2x');
369
370
            // convert to color index
371
            $fillColor = imagecolorallocate($image, $fillColor[0], $fillColor[1], $fillColor[2]);
372
373
            // fill background
374
            imagefill($image, 0, 0, $fillColor);
375 3
        } elseif (($this->getValue('MIMEType') == 'image/gif') || ($this->getValue('MIMEType') == 'image/png')) {
376 3
            $trans_index = imagecolortransparent($srcImage);
377
378
            // check if there is a specific transparent color
379 3
            if ($trans_index >= 0 && $trans_index < imagecolorstotal($srcImage)) {
380
                $trans_color = imagecolorsforindex($srcImage, $trans_index);
381
382
                // allocate in thumbnail
383
                $trans_index = imagecolorallocate($image, $trans_color['red'], $trans_color['green'], $trans_color['blue']);
384
385
                // fill background
386
                imagefill($image, 0, 0, $trans_index);
387
                imagecolortransparent($image, $trans_index);
388 3
            } elseif ($this->getValue('MIMEType') == 'image/png') {
389 3
                imagealphablending($image, false);
390 3
                $trans_color = imagecolorallocatealpha($image, 0, 0, 0, 127);
391 3
                imagefill($image, 0, 0, $trans_color);
392 3
                imagesavealpha($image, true);
393
            }
394
        }
395
396
        // resize photo to thumbnail
397 3
        if ($cropped) {
398
            imagecopyresampled(
399
                $image,
400
                $srcImage,
401
                ($thumbWidth - $scaledWidth) / 2,
402
                ($thumbHeight - $scaledHeight) / 2,
403
                0,
404
                0,
405
                $scaledWidth,
406
                $scaledHeight,
407
                $srcWidth,
408
                $srcHeight
409
            );
410
        } else {
411 3
            imagecopyresampled(
412 3
                $image,
413 3
                $srcImage,
414 3
                round(($thumbWidth - $scaledWidth) / 2),
415 3
                round(($thumbHeight - $scaledHeight) / 2),
416 3
                0,
417 3
                0,
418 3
                $scaledWidth,
419 3
                $scaledHeight,
420 3
                $srcWidth,
421 3
                $srcHeight
422 3
            );
423
        }
424
425
        // save thumbnail to disk
426 3
        switch ($this->getValue('ThumbnailMIMEType')) {
427 3
            case 'image/gif':
428
                imagegif($image, $thumbPath);
429
                break;
430
431 3
            case 'image/jpeg':
432
                imagejpeg($image, $thumbPath, static::$thumbnailJPEGCompression);
433
                break;
434
435 3
            case 'image/png':
436 3
                imagepng($image, $thumbPath, static::$thumbnailPNGCompression);
437 3
                break;
438
439
            default:
440
                throw new Exception('Unhandled thumbnail format');
441
        }
442
443 3
        chmod($thumbPath, static::$newFilePermissions);
444
    }
445
446
    // static methods
447
    public static function createFromUpload($uploadedFile, $fieldValues = []): static | false
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '|', expecting '{' or ';' on line 447 at column 86
Loading history...
448
    {
449
        // handle recieving a field array from $_FILES
450
        if (is_array($uploadedFile)) {
451
            if (isset($uploadedFile['error'])) {
452
                return false;
453
            }
454
455
            if (!empty($uploadedFile['name']) && empty($fieldValues['Caption'])) {
456
                $fieldValues['Caption'] = preg_replace('/\.[^.]+$/', '', $uploadedFile['name']);
457
            }
458
459
            $uploadedFile = $uploadedFile['tmp_name'];
460
        }
461
462
        // sanity check
463
        if (!is_uploaded_file($uploadedFile)) {
464
            throw new Exception('Supplied file is not a valid upload');
465
        }
466
467
        return static::createFromFile($uploadedFile, $fieldValues);
468
    }
469
470 3
    public static function createFromFile($file, $fieldValues = []): static | false
471
    {
472
        try {
473
            // handle url input
474 3
            if (filter_var($file, FILTER_VALIDATE_URL)) {
475
                $tempName = tempnam('/tmp', 'remote_media');
476
                copy($file, $tempName);
477
                $file = $tempName;
478
            }
479
480
            // analyze file
481 3
            $mediaInfo = static::analyzeFile($file);
482
483
            // create media object
484
            /**
485
             * @var static
486
             */
487 3
            $Media = $mediaInfo['className']::create($fieldValues);
488
489
            // init media
490 3
            $Media->initializeFromAnalysis($mediaInfo);
491
492
            // save media
493 3
            $Media->save();
494
495
            // write file
496 3
            $Media->writeFile($file);
497
498 3
            return $Media;
499
        } catch (Exception $e) {
500
            throw $e;
501
        }
502
503
        // remove photo record
504
        if ($Media) {
505
            $Media->destroy();
506
        }
507
508
        return false;
509
    }
510
511 3
    public function initializeFromAnalysis($mediaInfo)
512
    {
513 3
        $this->setValue('MIMEType', $mediaInfo['mimeType']);
514 3
        $this->setValue('Width', $mediaInfo['width']);
515 3
        $this->setValue('Height', $mediaInfo['height']);
516 3
        $this->setValue('Duration', $mediaInfo['duration']);
517
    }
518
519
520 3
    public static function analyzeFile($filename)
521
    {
522
        // DO NOT CALL FROM decendent's override, parent calls child
523
524
        // check file
525 3
        if (!is_readable($filename)) {
526
            throw new Exception('Unable to read media file for analysis: "'.$filename.'"');
527
        }
528
529
        // get mime type
530 3
        $finfo = finfo_open(FILEINFO_MIME_TYPE, static::$magicPath);
531
532 3
        if (!$finfo || !($mimeType = finfo_file($finfo, $filename))) {
533
            throw new Exception('Unable to load media file info');
534
        }
535
536 3
        finfo_close($finfo);
537
538
        // dig deeper if only generic mimetype returned
539 3
        if ($mimeType == 'application/octet-stream') {
540
            $finfo = finfo_open(FILEINFO_NONE, static::$magicPath);
541
542
            if (!$finfo || !($fileInfo = finfo_file($finfo, $filename))) {
543
                throw new Exception('Unable to load media file info');
544
            }
545
546
            finfo_close($finfo);
547
548
            // detect EPS
549
            if (preg_match('/^DOS EPS/i', $fileInfo)) {
550
                $mimeType = 'application/postscript';
551
            }
552 3
        } elseif (array_key_exists($mimeType, static::$mimeRewrites)) {
553
            $mimeType = static::$mimeRewrites[$mimeType];
554
        }
555
556
        // compile mime data
557 3
        $mediaInfo = [
558 3
            'mimeType' => $mimeType,
559 3
        ];
560
561
        // determine handler
562 3
        $staticClass = get_called_class();
563 3
        if (!isset(static::$mimeHandlers[$mediaInfo['mimeType']]) || $staticClass != __CLASS__) {
564
            throw new Exception('No class registered for mime type "' . $mediaInfo['mimeType'] . '"');
565
        } else {
566 3
            $className = $mediaInfo['className'] = static::$mimeHandlers[$mediaInfo['mimeType']];
567
568
            // call registered type's analyzer
569 3
            $mediaInfo = $className::analyzeFile($filename, $mediaInfo);
570
        }
571
572 3
        return $mediaInfo;
573
    }
574
575
    public static function getSupportedTypes(): array
576
    {
577
        return array_unique(array_merge(array_keys(static::$mimeHandlers), array_keys(static::$mimeRewrites)));
578
    }
579
580 17
    public function getFilesystemPath($variant = 'original', $filename = null): ?string
581
    {
582 17
        if ($this->isPhantom) {
583
            return null;
584
        }
585
586 17
        return App::$App->ApplicationPath.'/media/'.$variant.'/'.($filename ?: $this->getFilename($variant));
587
    }
588
589 17
    public function getFilename(): string
590
    {
591 17
        if ($this->isPhantom) {
592
            return 'default.'.$this->getValue('Extension');
593
        }
594
595 17
        return $this->getValue('ID').'.'.$this->getValue('Extension');
596
    }
597
598
    public function getMIMEType(): string
599
    {
600
        return $this->getValue('MIMEType');
601
    }
602
603 3
    public function writeFile($sourceFile): bool
604
    {
605 3
        $targetDirectory = dirname($this->getValue('FilesystemPath'));
606
607
        // create target directory if needed
608 3
        if (!is_dir($targetDirectory)) {
609 1
            mkdir($targetDirectory, static::$newDirectoryPermissions, true);
610
        }
611
612
        // move source file to target path
613 3
        if (!rename($sourceFile, $this->getValue('FilesystemPath'))) {
614
            throw new \Exception('Failed to move source file to destination');
615
        }
616
617
        // set file permissions
618 3
        return chmod($this->getValue('FilesystemPath'), static::$newFilePermissions);
619
    }
620
}
621