OSDN Git Service

b06eb64a0269b831bb5ec9d468e1567b2f3472fd
[nucleus-jp/nucleus-next.git] / nucleus / libs / MEDIA.php
1 <?php
2 /*
3  * Nucleus: PHP/MySQL Weblog CMS (http://nucleuscms.org/)
4  * Copyright (C) 2002-2012 The Nucleus Group
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  * (see nucleus/documentation/index.html#license for more info)
11  */
12 /**
13  * Media classes for nucleus
14  *
15  * @license http://nucleuscms.org/license.txt GNU General Public License
16  * @copyright Copyright (C) 2002-2012 The Nucleus Group
17  * @version $Id: MEDIA.php 1875 2012-06-17 07:30:44Z sakamocchi $
18  */
19
20 define('PRIVATE_COLLECTION',            'Private Collection');
21 define('READ_ONLY_MEDIA_FOLDER',        '(Read Only)');
22
23 class Media
24 {
25         static public $thumbdir = '.thumb';
26         static public $algorism = 'md5';
27         static public $image_mime = array(
28                 'image/jpeg'    => '.jpeg',
29                 'image/png'             => '.png',
30                 'image/gif'             => '.gif',
31         );
32         
33         /**
34          * Media::getCollectionList()
35          * Gets the list of collections available to the currently logged
36          * in member
37          * 
38          * @param       boolean $exceptReadOnly
39          * @return array        dirname => display name
40          */
41         static public function getCollectionList($exceptReadOnly = FALSE)
42         {
43                 global $member, $DIR_MEDIA;
44                 
45                 $collections = array();
46                 
47                 // add private directory for member
48                 $collections[$member->getID()] = PRIVATE_COLLECTION;
49                 
50                 // add global collections
51                 if ( !is_dir($DIR_MEDIA) )
52                 {
53                         return $collections;
54                 }
55                 
56                 $dirhandle = opendir($DIR_MEDIA);
57                 while ( $dirname = readdir($dirhandle) )
58                 {
59                         // only add non-numeric (numeric=private) dirs
60                         if ( @is_dir($DIR_MEDIA . $dirname) &&
61                                 ($dirname != '.') &&
62                                 ($dirname != '..') &&
63                                 ($dirname != self::$thumbdir) &&
64                                 (!is_numeric($dirname)) )
65                                 {
66                                 if ( @is_writable($DIR_MEDIA . $dirname) )
67                                 {
68                                         $collections[$dirname] = $dirname;
69                                 }
70                                 else if ( $exceptReadOnly == FALSE )
71                                 {
72                                         $collections[$dirname] = $dirname . ' ' . READ_ONLY_MEDIA_FOLDER;
73                                 }
74                         }
75                 }
76                 closedir($dirhandle);
77                 
78                 return $collections;
79         }
80         
81         /**
82          * Media::getMediaListByCollection()
83          * Returns an array of MediaObject objects for a certain collection
84          *
85          * @param       string  $collection     name of the collection
86          * @param       string  $filter         filter on filename (defaults to none)
87          * @return      void
88          */
89         static public function getMediaListByCollection($collection, $filter = '')
90         {
91                 global $CONF, $DIR_MEDIA;
92                 
93                 $filelist = array();
94                 
95                 // 1. go through all objects and add them to the filelist
96                 $mediadir = $DIR_MEDIA . $collection . '/';
97                 
98                 // return if dir does not exist
99                 if ( !is_dir($mediadir) )
100                 {
101                         return $filelist;
102                 }
103                 
104                 $dirhandle = opendir($mediadir);
105                 while ( $filename = readdir($dirhandle) )
106                 {
107                         // only add files that match the filter
108                         if ( !is_dir($mediadir . $filename) && self::checkFilter($filename, $filter) )
109                         {
110                                 array_push($filelist, new MediaObject($collection, $filename, $DIR_MEDIA));
111                         }
112                 }
113                 closedir($dirhandle);
114                 
115                 /* sort array */
116                 if ( !$CONF['MediaPrefix'] )
117                 {
118                         usort($filelist,  array(__CLASS__, 'sort_media_by_timestamp'));
119                 }
120                 else
121                 {
122                         usort($filelist,  array(__CLASS__, 'sort_media_by_filename'));
123                 }
124                 
125                 return $filelist;
126         }
127         
128         /**
129          * Media::checkFilter()
130          * 
131          * @param       string  $strText
132          * @param       string  $strFilter
133          * @return      boolean
134          */
135         static public function checkFilter($strText, $strFilter)
136         {
137                 if ( $strFilter == '' )
138                 {
139                         return 1;
140                 }
141                 else
142                 {
143                         return is_integer(i18n::strpos(strtolower($strText), strtolower($strFilter)));
144                 }
145         }
146         
147         /**
148          * Media::isValidCollection()
149          * checks if a collection exists with the given name, and if it's
150          * allowed for the currently logged in member to upload files to it
151          * 
152          * @param       string  $collectionName
153          * @param       string  $exceptReadOnly
154          * @return      boolean
155          */
156         static public function isValidCollection($collectionName, $exceptReadOnly = FALSE)
157         {
158                 global $member, $DIR_MEDIA;
159                 
160                 // allow creating new private directory
161                 if ( $collectionName === (string)$member->getID() )
162                 {
163                         return TRUE;
164                 }
165                 
166                 $collections = self::getCollectionList($exceptReadOnly);
167                 $dirname = $collections[$collectionName];
168                 
169                 if ( $dirname == NULL || $dirname === PRIVATE_COLLECTION )
170                 {
171                         return FALSE;
172                 }
173                 
174                 // other collections should exist and be writable
175                 $collectionDir = $DIR_MEDIA . $collectionName;
176                 if ( $exceptReadOnly )
177                 {
178                         return ( @is_dir($collectionDir) && @is_writable($collectionDir) );
179                 }
180                 
181                 // other collections should exist
182                 return @is_dir($collectionDir);
183         }
184         
185         /**
186          * Media::addMediaObject()
187          * Adds an uploaded file to the media archive
188          *
189          * @param       string  $collection     collection
190          * @param       array   $uploadfile     the postFileInfo(..) array
191          * @param       string  $filename       the filename that should be used to save the file as
192          *                                                              (date prefix should be already added here)
193          * @return      string  blank if success, message if failed
194          */
195         static public function addMediaObject($collection, $uploadfile, $filename)
196         {
197                 global $DIR_MEDIA, $manager;
198                 
199                 // clean filename of characters that may cause trouble in a filename using cleanFileName() function from globalfunctions.php
200                 $filename = cleanFileName($filename);
201                 
202                 // should already have tested for allowable types before calling this method. This will only catch files with no extension at all
203                 if ( $filename === FALSE )
204                 {
205                         return _ERROR_BADFILETYPE;
206                 }
207                 
208                 // trigger PreMediaUpload event
209                 $manager->notify('PreMediaUpload',array('collection' => &$collection, 'uploadfile' => $uploadfile, 'filename' => &$filename));
210                 
211                 // don't allow uploads to unknown or forbidden collections
212                 $exceptReadOnly = TRUE;
213                 if ( !self::isValidCollection($collection,$exceptReadOnly) )
214                 {
215                         return _ERROR_DISALLOWED;
216                 }
217                 
218                 // check dir permissions (try to create dir if it does not exist)
219                 $mediadir = $DIR_MEDIA . $collection;
220                 
221                 // try to create new private media directories if needed
222                 if ( !@is_dir($mediadir) && is_numeric($collection) )
223                 {
224                         $oldumask = umask(0000);
225                         if ( !@mkdir($mediadir, 0777) )
226                         {
227                                 return _ERROR_BADPERMISSIONS;
228                         }
229                         umask($oldumask);
230                 }
231                 
232                 // if dir still not exists, the action is disallowed
233                 if ( !@is_dir($mediadir) )
234                 {
235                         return _ERROR_DISALLOWED;
236                 }
237                 
238                 if ( !is_writeable($mediadir) )
239                 {
240                         return _ERROR_BADPERMISSIONS;
241                 }
242                 
243                 // add trailing slash (don't add it earlier since it causes mkdir to fail on some systems)
244                 $mediadir .= '/';
245                 
246                 if ( file_exists($mediadir . $filename) )
247                 {
248                         return _ERROR_UPLOADDUPLICATE;
249                 }
250                 
251                 // move file to directory
252                 if ( is_uploaded_file($uploadfile) )
253                 {
254                         if ( !@move_uploaded_file($uploadfile, $mediadir . $filename) )
255                         {
256                                 return _ERROR_UPLOADMOVEP;
257                         }
258                 }
259                 else
260                 {
261                         if ( !copy($uploadfile, $mediadir . $filename) )
262                         {
263                                 return _ERROR_UPLOADCOPY ;
264                         }
265                 }
266                 
267                 // chmod uploaded file
268                 $oldumask = umask(0000);
269                 @chmod($mediadir . $filename, 0644);
270                 umask($oldumask);
271                 
272                 $manager->notify('PostMediaUpload',array('collection' => $collection, 'mediadir' => $mediadir, 'filename' => $filename));
273                 
274                 return '';
275         }
276         
277         /**
278          * Media::addMediaObjectRaw()
279          * Adds an uploaded file to the media dir.
280          * 
281          * NOTE: does not check if $collection is valid.
282          * 
283          * @param       string  $collection     collection to use
284          * @param       string  $filename       the filename that should be used to save the file
285          *                                                              as (date prefix should be already added here)
286          * @param       &$data  File data (binary)
287          * @return      string  blank if success, message if failed
288          */
289         static public function addMediaObjectRaw($collection, $filename, &$data)
290         {
291                 global $DIR_MEDIA;
292                 
293                 // check dir permissions (try to create dir if it does not exist)
294                 $mediadir = $DIR_MEDIA . $collection;
295                 
296                 // try to create new private media directories if needed
297                 if ( !@is_dir($mediadir) && is_numeric($collection) )
298                 {
299                         $oldumask = umask(0000);
300                         if ( !@mkdir($mediadir, 0777) )
301                         {
302                                 return _ERROR_BADPERMISSIONS;
303                         }
304                         umask($oldumask);
305                 }
306                 
307                 // if dir still not exists, the action is disallowed
308                 if ( !@is_dir($mediadir) )
309                 {
310                         return _ERROR_DISALLOWED;
311                 }
312                 
313                 if ( !is_writeable($mediadir) )
314                 {
315                         return _ERROR_BADPERMISSIONS;
316                 }
317                 
318                 // add trailing slash (don't add it earlier since it causes mkdir to fail on some systems)
319                 $mediadir .= '/';
320                 
321                 if ( file_exists($mediadir . $filename) )
322                 {
323                         return _ERROR_UPLOADDUPLICATE;
324                 }
325                 
326                 // create file
327                 $fh = @fopen($mediadir . $filename, 'wb');
328                 if ( !$fh )
329                 {
330                         return _ERROR_UPLOADFAILED;
331                 }
332                 $ok = @fwrite($fh, $data);
333                 @fclose($fh);
334                 if ( !$ok )
335                 {
336                         return _ERROR_UPLOADFAILED;
337                 }
338                 
339                 // chmod uploaded file
340                 $oldumask = umask(0000);
341                 @chmod($mediadir . $filename, 0644);
342                 umask($oldumask);
343                 
344                 return '';
345         }
346         
347         /**
348          * Media::responseResampledImage()
349          * send resampled image via HTTP
350          * 
351          * @param       object  $medium         MediaObject Object
352          * @exit
353          */
354         static public function responseResampledImage($medium, $maxwidth=0, $maxheight=0)
355         {
356                 if ( get_class($medium) !== 'MediaObject' )
357                 {
358                         header("HTTP/1.1 500 Internal Server Error");
359                         exit('Nucleus CMS: Fail to generate resampled image');
360                         return;
361                 }
362                 
363                 $resampledimage = $medium->getResampledBinary($maxwidth, $maxheight);
364                 if ( $resampledimage === FALSE )
365                 {
366                         unset($resampledimage);
367                         header("HTTP/1.1 503 Service Unavailable");
368                         exit('Nucleus CMS: Fail to generate resampled image');
369                         return;
370                 }
371                 
372                 header("Content-type: {$medium->mime}");
373                 echo $resampledimage;
374                 
375                 unset($resampledimage);
376                 
377                 exit;
378         }
379         
380         /**
381          * Media::storeResampledImage()
382          * Store resampled image binary to filesystem as file
383          * 
384          * @param       object  $medium         MediaObject Object
385          * @param       integer $maxwidth       maximum width
386          * @param       integer $maxheight      maximum height
387          * @param       string  $path           directory path for destination
388          * @param       string  $name           file name for destination
389          * @return      boolean
390          */
391         static public function storeResampledImage($medium, $maxwidth=0, $maxheight=0, $path='', $name='')
392         {
393                 global $DIR_MEDIA;
394                 
395                 if ( get_class($medium) !== 'MediaObject' )
396                 {
397                         return FALSE;
398                 }
399                 
400                 if ( $path !== '' )
401                 {
402                         $path = realpath($path);
403                         if ( !file_exists($path)
404                           || strpos($path, $DIR_MEDIA) !== 0 )
405                         {
406                                 return FALSE;
407                         }
408                 }
409                 else
410                 {
411                         $path = '$DIR_MEDIA/' . self::$thumbdir;
412                 }
413                 
414                 if ( $name === '' )
415                 {
416                         $name = $medium->getHashedname();
417                 }
418                 
419                 $resampledimage = $medium->getResampledBinary($maxwidth, $maxheight);
420                 if ( !$resampledimage )
421                 {
422                         unset($resampledimage);
423                         return FALSE;
424                 }
425                 
426                 $handle = @fopen("{$path}/{$name}", 'w');
427                 if ( !$handle )
428                 {
429                         unset ($resampledimage);
430                         return FALSE;
431                 }
432                 
433                 if ( !@fwrite($handle, $resampledimage) )
434                 {
435                         unset($resampledimage);
436                         @unlink("{$path}/{$name}");
437                         return FALSE;
438                 }
439                 
440                 unset($resampledimage);
441                 fclose($handle);
442                 
443                 if ( !@chmod("{$path}/{$name}", 0774) )
444                 {
445                         @unlink("{$path}/{$name}");
446                         return FALSE;
447                 }
448                 
449                 return TRUE;
450         }
451         
452         /**
453          * Media::sort_media_by_timestamp()
454          * User-defined sort method to sort an array of MediaObjects
455          * 
456          * @param       object  $a
457          * @param       object  $b
458          * @return      boolean
459          */
460         static private function sort_media_by_timestamp($a, $b)
461         {
462                 if ($a->timestamp == $b->timestamp) return 0;
463                 return ($a->timestamp > $b->timestamp) ? -1 : 1;
464         }
465         
466         /**
467          * Media::sort_media_by_filename()
468          * User-defined sort method to sort an array of MediaObjects
469          * 
470          * @param       object  $a
471          * @param       object  $b
472          * @return      boolean
473          */
474         static private function sort_media_by_filename($a, $b)
475         {
476                 if ($a->filename == $b->filename) return 0;
477                 return ($a->filename > $b->filename) ? -1 : 1;
478         }
479 }
480
481 class MediaObject
482 {
483         public $mime = '';
484         
485         public $root = '';
486         public $path = '';
487         public $private;
488         public $collection;
489         public $filename = '';
490         
491         public $prefix = '';
492         public $name = '';
493         public $suffix = '';
494         
495         public $timestamp = 0;
496         public $size = 0;
497         
498         public $width = 0;
499         public $height = 0;
500         public $resampledwidth = 0;
501         public $resampledheight = 0;
502         
503         /**
504          * MediaObject::__construct()
505          * 
506          * @param       string          $collection     
507          * @param       string          $filename       
508          * @param       string          $root           fullpath to media directory
509          */
510         public function __construct($collection, $filename, $root=0)
511         {
512                 global $CONF, $DIR_MEDIA;
513                 
514                 /* for backward compatibility */
515                 if ( is_numeric($root) )
516                 {
517                         $root = $DIR_MEDIA;
518                 }
519                 
520                 $root = preg_replace('#/*$#', '', $root);
521                 
522                 /* get and validate fullpath for the medium */
523                 if ( !file_exists($root)
524                   || FALSE === ($fullpath = realpath("{$root}/{$collection}/{$filename}"))
525                   || strpos($fullpath, $root) !== 0
526                   || !file_exists($fullpath) )
527                 {
528                         return FALSE;
529                 }
530                 
531                 /* store fundamentals */
532                 $this->root = $root;
533                 $this->private = (integer) $collection;
534                 $this->collection = $collection;
535                 $this->filename = basename($fullpath);
536                 $this->timestamp = filemtime($fullpath);
537                 
538                 /* store relative directory path from root directory for media */
539                 $this->path = preg_replace(array("#{$this->root}/#", "#/{$this->filename}#"), '', $fullpath);
540                 if ( $this->path === $this->name )
541                 {
542                         $this->path = ''; 
543                 }
544                 
545                 return;
546         }
547         
548         /**
549          * MediaObject::refine()
550          * refine data
551          * 
552          * @param       void
553          * @return      void
554          */
555         public function refine()
556         {
557                 global $CONF;
558                 
559                 /* store size (byte order) */
560                 $this->size = filesize("{$this->root}/{$this->path}/{$this->filename}");
561                 
562                 /* get width and height if this is image binary */
563                 if ( FALSE === ($info = @getimagesize ("{$this->root}/{$this->path}/{$this->filename}")) )
564                 {
565                         $this->mime = 'application/octet-stream';
566                         $this->width = 0;
567                         $this->height = 0;
568                 }
569                 else
570                 {
571                         $this->mime = $info['mime'];
572                         $this->width = $info[0];
573                         $this->height = $info[1];
574                 }
575                 
576                 /* utilise Fileinfo subsystem if available */
577                 if ( defined('FILEINFO_MIME_TYPE') && function_exists ('finfo_open')
578                   && (FALSE !== ($info = finfo_open(FILEINFO_MIME_TYPE))) )
579                 {
580                         $this->mime = finfo_file($info, "{$this->root}/{$this->path}/{$this->filename}");
581                 }
582                 
583                 /* store data with parsed filename */
584                 if ( preg_match('#^(.*)\.([a-zA-Z0-9]{2,})$#', $this->filename, $info) === 1 )
585                 {
586                         $this->name = $info[1];
587                         $this->suffix = $info[2];
588                         
589                         if ( $CONF['MediaPrefix'] && preg_match('#^([0-9]{8})\-(.*)$#', $this->name, $info) == 1 )
590                         {
591                                 $this->prefix = preg_replace('#^([0-9]{4})([0-9]{2})([0-9]{2})$#', '$1/$2/$3', $info[1]);
592                                 $this->name = $info[2];
593                         }
594                 }
595                 
596                 return;
597         }
598         
599         /**
600          * MediaObject::setResampledSize()
601          * Set resampled size
602          * 
603          * @param       integer $maxwidth
604          * @param       integer $maxheight
605          * @return      boolean
606          */
607         public function setResampledSize($maxwidth=0, $maxheight=0)
608         {
609                 if ( ($maxwidth == 0) && ($maxheight == 0) )
610                 {
611                         return FALSE;
612                 }
613                 else if ( $this->width == 0 || $this->height  == 0 )
614                 {
615                         return FALSE;
616                 }
617                 else if ($this->width < $maxwidth && $this->height < $maxheight )
618                 {
619                         $this->resampledwidth = $this->width;
620                         $this->resampledheight = $this->height;
621                 }
622                 else if ( $maxheight == 0 || $this->width > $this->height )
623                 {
624                         $this->resampledheight = intval ($this->height * $maxwidth / $this->width);
625                         $this->resampledwidth = $maxwidth;
626                 }
627                 else if ( $maxwidth == 0 || $this->width <= $this->height )
628                 {
629                         $this->resampledwidth = intval ($this->width * $maxheight / $this->height);
630                         $this->resampledheight = $maxheight;
631                 }
632                 return TRUE;
633         }
634         
635         /**
636          * MediaObject::getResampledBinary()
637          * Return resampled image binary
638          * 
639          * @param       void
640          * @return      mixed   binary if success, FALSE if failed
641          */
642         public function getResampledBinary($maxwidth=0, $maxheight=0)
643         {
644                 static $gdinfo = array();
645                 static $original;
646                 static $resampledimage;
647                 
648                 if ( !$this->setResampledSize($maxwidth, $maxheight) )
649                 {
650                         return FALSE;
651                 }
652                 
653                 if ( $gdinfo = array() )
654                 {
655                         $gdinfo = gd_info();
656                 }
657                 
658                 if ( $this->path !== '' )
659                 {
660                         $fullpath = "{$this->root}/{$this->path}/{$this->name}";
661                 }
662                 else
663                 {
664                         $fullpath = "{$this->root}/{$this->name}";
665                 }
666                 if ( !file_exists($fullpath) )
667                 {
668                         return FALSE;
669                 }
670                 
671                 if ( !array_key_exists($this->mime, Media::$image_mime)
672                   || $this->width == 0
673                   || $this->height == 0
674                   || $this->resampledwidth == 0
675                   || $this->resampledheight == 0 )
676                 {
677                         return FALSE;
678                 }
679                 
680                 /* check current available memory */
681                 $memorymax = trim(ini_get("memory_limit"));
682                 switch ( strtolower ($memorymax[strlen($memorymax)-1]) )
683                 {
684                         case 'g':
685                                 $memorymax *= 1024;
686                         case 'm':
687                                 $memorymax *= 1024;
688                         case 'k':
689                                 $memorymax *= 1024;
690                 }
691                 
692                 /*
693                  * this code is based on analyze if gd.c in php source code
694                  * if you can read C/C++, please check these elements and notify us if you have some ideas
695                  */
696                 if ( (memory_get_usage()
697                    + ($this->resampledwidth * $this->resampledheight * 5 + $this->resampledheight * 24 + 10000)
698                    + ($this->width * $this->height * 5 + $this->height * 24 + 10000))
699                   > $memorymax )
700                 {
701                         return FALSE;
702                 }
703                 
704                 switch ( $this->mime )
705                 {
706                         case 'image/gif':
707                                 if ( (!array_key_exists('GIF Read Support', $gdinfo) || !isset($gdinfo['GIF Read Support']))
708                                   || (!array_key_exists('GIF Create Support', $gdinfo) || !isset($gdinfo['GIF Create Support'])) )
709                                 {
710                                         return FALSE;
711                                 }
712                                 $function = 'imagecreatefromgif';
713                                 break;
714                         case 'image/jpeg':
715                                 if ( (!array_key_exists('JPEG Support', $gdinfo) || !isset($gdinfo['JPEG Support']))
716                                   && (!array_key_exists('JPG Support', $gdinfo) || !isset($gdinfo['JPG Support'])) )
717                                 {
718                                         return FALSE;
719                                 }
720                                 $function = 'imagecreatefromjpeg';
721                                 break;
722                         case 'image/png':
723                                 if ( !array_key_exists('PNG Support', $gdinfo) || !isset($gdinfo['PNG Support']) )
724                                 {
725                                         return FALSE;
726                                 }
727                                 $function = 'imagecreatefrompng';
728                                 break;
729                         default:
730                                 return FALSE;
731                 }
732                 
733                 if ( !is_callable($function) )
734                 {
735                         return FALSE;
736                 }
737                 
738                 $original = call_user_func_array($function, array(&$fullpath));
739                 if ( !$original )
740                 {
741                         return FALSE;
742                 }
743                 
744                 $resampledimage = imagecreatetruecolor($this->resampledwidth, $this->resampledheight);
745                 if ( !$resampledimage )
746                 {
747                         imagedestroy($original);
748                         return FALSE;
749                 }
750                 
751                 @set_time_limit(ini_get('max_execution_time'));
752                 if ( !ImageCopyResampled($resampledimage, $original, 0, 0, 0, 0, $this->resampledwidth, $this->resampledheight, $this->width, $this->height) )
753                 {
754                         return FALSE;
755                 }
756                 
757                 imagedestroy($original);
758                 
759                 ob_start();
760                 
761                 switch ( $this->mime )
762                 {
763                         case 'image/gif':
764                                 imagegif($resampledimage);
765                                 break;
766                         case 'image/jpeg':
767                                 imagejpeg($resampledimage);
768                                 break;
769                         case 'image/png':
770                                 imagepng($resampledimage);
771                                 break;
772                         case 'image/bmp':
773                         case 'image/x-ms-bmp':
774                                 imagepng($resampledimage);
775                                 break;
776                         default:
777                                 return FALSE;
778                 }
779                 
780                 imagedestroy($resampledimage);
781                 
782                 return ob_get_clean();
783         }
784         
785         public function getHashedName()
786         {
787                 return (string) hash(Media::$algorism, "{$this->path}/{$this->name}", FALSE);
788         }
789 }