Passed
Push — develop ( 47d001...0a7564 )
by Henry
13:07
created

Media::analyzeFile()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 58
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 18.744

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 24
c 1
b 0
f 0
nc 11
nop 1
dl 0
loc 58
ccs 15
cts 25
cp 0.6
crap 18.744
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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