1 <?php
  2 
  3 /**
  4  * @package redaxo\media-manager
  5  */
  6 class rex_managed_media
  7 {
  8     private $media_path = '';
  9     private $media;
 10     private $asImage = false;
 11     private $image;
 12     private $header = [];
 13     private $sourcePath;
 14     private $format;
 15 
 16     private $mimetypeMap = [
 17         'image/jpeg' => 'jpg',
 18         'image/jpg' => 'jpg',
 19         'image/pjpeg' => 'jpg',
 20         'image/vnd.wap.wbmp' => 'wbmp',
 21         'image/png' => 'png',
 22         'image/gif' => 'gif',
 23         'image/webp' => 'webp',
 24     ];
 25 
 26     public function __construct($media_path)
 27     {
 28         $this->setMediaPath($media_path);
 29         $this->format = strtolower(rex_file::extension($this->getMediaPath()));
 30     }
 31 
 32     /**
 33      * Returns the original path of the media.
 34      *
 35      * To get the current source path (can be changed by effects) use `getSourcePath` instead.
 36      *
 37      * @return null|string
 38      */
 39     public function getMediaPath()
 40     {
 41         return $this->media_path;
 42     }
 43 
 44     public function setMediaPath($media_path)
 45     {
 46         $this->media_path = $media_path;
 47 
 48         if (null === $media_path) {
 49             return;
 50         }
 51 
 52         $this->media = basename($media_path);
 53         $this->asImage = false;
 54 
 55         if (file_exists($media_path)) {
 56             $this->sourcePath = $media_path;
 57         } else {
 58             $this->sourcePath = rex_path::addon('media_manager', 'media/warning.jpg');
 59         }
 60     }
 61 
 62     public function getMediaFilename()
 63     {
 64         return $this->media;
 65     }
 66 
 67     public function setMediaFilename($filename)
 68     {
 69         $this->media = $filename;
 70     }
 71 
 72     public function setHeader($type, $content)
 73     {
 74         $this->header[$type] = $content;
 75     }
 76 
 77     public function getHeader()
 78     {
 79         return $this->header;
 80     }
 81 
 82     public function asImage()
 83     {
 84         if ($this->asImage) {
 85             return;
 86         }
 87 
 88         $this->asImage = true;
 89 
 90         $this->image = [];
 91         $this->image['src'] = false;
 92 
 93         // if mimetype detected and in imagemap -> change format
 94         if (class_exists('finfo') && $finfo = new finfo(FILEINFO_MIME_TYPE)) {
 95             if ($ftype = @$finfo->file($this->getSourcePath())) {
 96                 if (array_key_exists($ftype, $this->mimetypeMap)) {
 97                     $this->format = $this->mimetypeMap[$ftype];
 98                 }
 99             }
100         }
101 
102         if ($this->format == 'jpg' || $this->format == 'jpeg') {
103             $this->format = 'jpeg';
104             $this->image['src'] = @imagecreatefromjpeg($this->getSourcePath());
105         } elseif ($this->format == 'gif') {
106             $this->image['src'] = @imagecreatefromgif($this->getSourcePath());
107         } elseif ($this->format == 'wbmp') {
108             $this->image['src'] = @imagecreatefromwbmp($this->getSourcePath());
109         } elseif ($this->format == 'webp') {
110             if (function_exists('imagecreatefromwebp')) {
111                 $this->image['src'] = @imagecreatefromwebp($this->getSourcePath());
112                 imagealphablending($this->image['src'], false);
113                 imagesavealpha($this->image['src'], true);
114             }
115         } else {
116             $this->image['src'] = @imagecreatefrompng($this->getSourcePath());
117             if ($this->image['src']) {
118                 imagealphablending($this->image['src'], false);
119                 imagesavealpha($this->image['src'], true);
120                 $this->format = 'png';
121             }
122         }
123 
124         if (!$this->image['src']) {
125             $this->setSourcePath(rex_path::addon('media_manager', 'media/warning.jpg'));
126             $this->asImage();
127         } else {
128             $this->fixOrientation();
129             $this->refreshImageDimensions();
130         }
131     }
132 
133     public function refreshImageDimensions()
134     {
135         // getimagesize does not work for webp with PHP < 7.1
136         if (!$this->asImage && 'webp' === $this->format && PHP_VERSION_ID < 70100) {
137             $this->asImage();
138         }
139 
140         if ($this->asImage) {
141             $this->image['width'] = imagesx($this->image['src']);
142             $this->image['height'] = imagesy($this->image['src']);
143 
144             return;
145         }
146 
147         if ('jpeg' !== $this->format && !in_array($this->format, $this->mimetypeMap)) {
148             return;
149         }
150 
151         $size = getimagesize($this->sourcePath);
152         $this->image['width'] = $size[0];
153         $this->image['height'] = $size[1];
154     }
155 
156     public function getFormat()
157     {
158         return $this->format;
159     }
160 
161     public function setFormat($format)
162     {
163         $this->format = $format;
164     }
165 
166     public function sendMedia($sourceCacheFilename, $headerCacheFilename, $save = false)
167     {
168         $this->prepareHeaders();
169 
170         if ($this->asImage) {
171             $src = $this->getSource();
172             $this->setHeader('Content-Length', rex_string::size($src));
173 
174             rex_response::cleanOutputBuffers();
175             foreach ($this->header as $t => $c) {
176                 header($t . ': ' . $c);
177             }
178 
179             echo $src;
180 
181             if ($save) {
182                 rex_file::putCache($headerCacheFilename, [
183                     'media_path' => $this->getMediaPath(),
184                     'format' => $this->format,
185                     'headers' => $this->header,
186                 ]);
187 
188                 rex_file::put($sourceCacheFilename, $src);
189             }
190         } else {
191             $this->setHeader('Content-Length', filesize($this->getSourcePath()));
192 
193             rex_response::cleanOutputBuffers();
194             foreach ($this->header as $t => $c) {
195                 rex_response::setHeader($t, $c);
196             }
197 
198             rex_response::sendFile($this->getSourcePath(), $this->header['Content-Type']);
199 
200             if ($save) {
201                 rex_file::putCache($headerCacheFilename, [
202                     'media_path' => $this->getMediaPath(),
203                     'format' => $this->format,
204                     'headers' => $this->header,
205                 ]);
206 
207                 rex_file::copy($this->getSourcePath(), $sourceCacheFilename);
208             }
209         }
210     }
211 
212     public function save($sourceCacheFilename, $headerCacheFilename)
213     {
214         $src = $this->getSource();
215 
216         $this->prepareHeaders($src);
217         $this->saveFiles($src, $sourceCacheFilename, $headerCacheFilename);
218     }
219 
220     protected function getImageSource()
221     {
222         $addon = rex_addon::get('media_manager');
223 
224         $format = $this->format;
225         $format = 'jpeg' === $format ? 'jpg' : $format;
226 
227         $interlace = $this->getImageProperty('interlace', $addon->getConfig('interlace', ['jpg']));
228         imageinterlace($this->image['src'], in_array($format, $interlace) ? 1 : 0);
229 
230         ob_start();
231         if ($format == 'jpg') {
232             $quality = $this->getImageProperty('jpg_quality', $addon->getConfig('jpg_quality', 85));
233             imagejpeg($this->image['src'], null, $quality);
234         } elseif ($format == 'png') {
235             $compression = $this->getImageProperty('png_compression', $addon->getConfig('png_compression', 5));
236             imagepng($this->image['src'], null, $compression);
237         } elseif ($format == 'gif') {
238             imagegif($this->image['src']);
239         } elseif ($format == 'wbmp') {
240             imagewbmp($this->image['src']);
241         } elseif ($format == 'webp') {
242             $quality = $this->getImageProperty('webp_quality', $addon->getConfig('webp_quality', 85));
243             imagewebp($this->image['src'], null, $quality);
244         }
245         return ob_get_clean();
246     }
247 
248     public function getImage()
249     {
250         return $this->image['src'];
251     }
252 
253     public function setImage($src)
254     {
255         $this->image['src'] = $src;
256         $this->asImage = true;
257     }
258 
259     public function setSourcePath($path)
260     {
261         $this->sourcePath = $path;
262 
263         $this->asImage = false;
264 
265         if (isset($this->image['src']) && is_resource($this->image['src'])) {
266             imagedestroy($this->image['src']);
267         }
268     }
269 
270     /**
271      * Returns the current source path.
272      *
273      * To get the original media path use `getMediaPath()` instead.
274      *
275      * @return string
276      */
277     public function getSourcePath()
278     {
279         return $this->sourcePath;
280     }
281 
282     /**
283      * @return string
284      */
285     public function getSource()
286     {
287         if ($this->asImage) {
288             return $this->getImageSource();
289         }
290 
291         return rex_file::get($this->sourcePath);
292     }
293 
294     public function setImageProperty($name, $value)
295     {
296         $this->image[$name] = $value;
297     }
298 
299     public function getImageProperty($name, $default = null)
300     {
301         return isset($this->image[$name]) ? $this->image[$name] : $default;
302     }
303 
304     public function getWidth()
305     {
306         return $this->image['width'];
307     }
308 
309     public function getHeight()
310     {
311         return $this->image['height'];
312     }
313 
314     /**
315      * @deprecated since 2.3.0, use `getWidth()` instead
316      */
317     public function getImageWidth()
318     {
319         return $this->getWidth();
320     }
321 
322     /**
323      * @deprecated since 2.3.0, use `getHeight()` instead
324      */
325     public function getImageHeight()
326     {
327         return $this->getHeight();
328     }
329 
330     private function fixOrientation()
331     {
332         if (!function_exists('exif_read_data')) {
333             return;
334         }
335         // exif_read_data() only works on jpg/jpeg/tiff
336         if (!in_array($this->getFormat(), ['jpg', 'jpeg', 'tiff'])) {
337             return;
338         }
339         // suppress warning in case of corrupt/ missing exif data
340         $exif = @exif_read_data($this->getSourcePath());
341 
342         if (!isset($exif['Orientation']) || !in_array($exif['Orientation'], [3, 6, 8])) {
343             return;
344         }
345 
346         switch ($exif['Orientation']) {
347             case 8:
348                 $this->image['src'] = imagerotate($this->image['src'], 90, 0);
349                 break;
350             case 3:
351                 $this->image['src'] = imagerotate($this->image['src'], 180, 0);
352                 break;
353             case 6:
354                 $this->image['src'] = imagerotate($this->image['src'], -90, 0);
355                 break;
356         }
357     }
358 
359     /**
360      * @param string $src Source content
361      */
362     private function prepareHeaders($src = null)
363     {
364         if ($src !== null) {
365             $this->setHeader('Content-Length', rex_string::size($src));
366         }
367 
368         $header = $this->getHeader();
369         if (!isset($header['Content-Type'])) {
370             $content_type = '';
371 
372             if (!$content_type && function_exists('mime_content_type')) {
373                 $content_type = mime_content_type($this->getSourcePath());
374             }
375 
376             if (!$content_type && function_exists('finfo_open')) {
377                 $finfo = finfo_open(FILEINFO_MIME_TYPE);
378                 $content_type = finfo_file($finfo, $this->getSourcePath());
379             }
380 
381             // In case mime_content_type() returns 'text/plain' for CSS / JS files:
382             if ($content_type == 'text/plain') {
383                 if (pathinfo($this->getSourcePath(), PATHINFO_EXTENSION) == 'css') {
384                     $content_type = 'text/css';
385                 } elseif (pathinfo($this->getSourcePath(), PATHINFO_EXTENSION) == 'js') {
386                     $content_type = 'application/javascript';
387                 }
388             }
389 
390             if ($content_type != '') {
391                 $this->setHeader('Content-Type', $content_type);
392             }
393         }
394         if (!isset($header['Content-Disposition'])) {
395             $this->setHeader('Content-Disposition', 'inline; filename="' . $this->getMediaFilename() . '";');
396         }
397         if (!isset($header['Last-Modified'])) {
398             $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s T'));
399         }
400     }
401 
402     /**
403      * @param string $src                 Source content
404      * @param string $sourceCacheFilename
405      * @param string $headerCacheFilename
406      */
407     private function saveFiles($src, $sourceCacheFilename, $headerCacheFilename)
408     {
409         rex_file::putCache($headerCacheFilename, [
410             'media_path' => $this->getMediaPath(),
411             'format' => $this->format,
412             'headers' => $this->header,
413         ]);
414 
415         rex_file::put($sourceCacheFilename, $src);
416     }
417 }
418