]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Picture.php
Merge branch 'hotfix/0.7.0.2' into develop
[galette.git] / galette / lib / Galette / Core / Picture.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Picture handling
7 *
8 * PHP version 5
9 *
10 * Copyright © 2006-2012 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Core
28 * @package Galette
29 *
30 * @author Frédéric Jaqcuot <unknown@unknow.com>
31 * @author Johan Cwiklinski <johan@x-tnd.be>
32 * @copyright 2006-2012 The Galette Team
33 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
34 * @version SVN: $Id$
35 * @link http://galette.tuxfamily.org
36 */
37
38 namespace Galette\Core;
39
40 use Galette\Entity\Adherent;
41 use Galette\Common\KLogger as KLogger;
42
43 /**
44 * Picture handling
45 *
46 * @name Picture
47 * @category Core
48 * @package Galette
49 * @author Frédéric Jaqcuot <unknown@unknow.com>
50 * @author Johan Cwiklinski <johan@x-tnd.be>
51 * @copyright 2006-2012 The Galette Team
52 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
53 * @link http://galette.tuxfamily.org
54 */
55 class Picture
56 {
57 //constants that will not be overrided
58 const INVALID_FILE = -1;
59 const INVALID_EXTENSION = -2;
60 const FILE_TOO_BIG = -3;
61 const MIME_NOT_ALLOWED = -4;
62 const SQL_ERROR = -5;
63 const SQL_BLOB_ERROR = -6;
64 //constants that can be overrided
65 //(do not use self::CONSTANT, but get_class[$this]::CONSTANT)
66 const MAX_FILE_SIZE = 1024;
67 const TABLE = 'pictures';
68 const PK = Adherent::PK;
69
70 /*private $_bad_chars = array(
71 '\.', '\\\\', "'", ' ', '\/', ':', '\*', '\?', '"', '<', '>', '|'
72 );*/
73 //array keys contain litteral value of each forbidden character
74 //(to be used when showing an error).
75 //Maybe is there a better way to handle this...
76 private $_bad_chars = array(
77 '.' => '\.',
78 '\\' => '\\\\',
79 "'" => "'",
80 ' ' => ' ',
81 '/' => '\/',
82 ':' => ':',
83 '*' => '\*',
84 '?' => '\?',
85 '"' => '"',
86 '<' => '<',
87 '>' => '>',
88 '|' => '|'
89 );
90 private $_allowed_extensions = array('jpeg', 'jpg', 'png', 'gif');
91 private $_allowed_mimes = array(
92 'jpg' => 'image/jpeg',
93 'png' => 'image/png',
94 'gif' => 'image/gif'
95 );
96
97 protected $tbl_prefix = '';
98
99 protected $id;
100 protected $height;
101 protected $width;
102 protected $optimal_height;
103 protected $optimal_width;
104 protected $file_path;
105 protected $format;
106 protected $mime;
107 protected $has_picture = true;
108 protected $store_path = GALETTE_PHOTOS_PATH;
109 protected $max_width = 200;
110 protected $max_height = 200;
111
112 /**
113 * Default constructor.
114 *
115 * @param int $id_adh the id of the member
116 */
117 public function __construct( $id_adh='' )
118 {
119 // '!==' needed, otherwise ''==0
120 if ( $id_adh !== '' ) {
121 $this->id = $id_adh;
122 if ( !isset ($this->db_id) ) {
123 $this->db_id = $id_adh;
124 }
125
126 //if file does not exists on the FileSystem, check for it in the database
127 if ( !$this->_checkFileOnFS() ) {
128 $this->_checkFileInDB();
129 }
130 }
131
132 // if we still have no picture, take the default one
133 if ( $this->file_path=='' ) {
134 $this->getDefaultPicture();
135 }
136
137 //we should not have an empty file_path, but...
138 if ( $this->file_path !== '' ) {
139 $this->_setSizes();
140 }
141 }
142
143 /**
144 * "Magic" function called on unserialize
145 *
146 * @return void
147 */
148 public function __wakeup()
149 {
150 //if file has been deleted since we store our object in the session,
151 //we try to retrieve it
152 if ( !$this->_checkFileOnFS() ) {
153 //if file does not exists on the FileSystem,
154 //check for it in the database
155 //$this->_checkFileInDB();
156 }
157
158 // if we still have no picture, take the default one
159 if ( $this->file_path=='' ) {
160 $this->getDefaultPicture();
161 }
162
163 //we should not have an empty file_path, but...
164 if ( $this->file_path !== '' ) {
165 $this->_setSizes();
166 }
167 }
168
169 /**
170 * Check if current file is present on the File System
171 *
172 * @return boolean true if file is present on FS, false otherwise
173 */
174 private function _checkFileOnFS()
175 {
176 $file_wo_ext = $this->store_path . $this->id;
177 if ( file_exists($file_wo_ext . '.jpg') ) {
178 $this->file_path = $file_wo_ext . '.jpg';
179 $this->format = 'jpg';
180 $this->mime = 'image/jpeg';
181 return true;
182 } elseif ( file_exists($file_wo_ext . '.png') ) {
183 $this->file_path = $file_wo_ext . '.png';
184 $this->format = 'png';
185 $this->mime = 'image/png';
186 return true;
187 } elseif ( file_exists($file_wo_ext . '.gif') ) {
188 $this->file_path = $file_wo_ext . '.gif';
189 $this->format = 'gif';
190 $this->mime = 'image/gif';
191 return true;
192 }
193 return false;
194 }
195
196 /**
197 * Check if current file is present in the database,
198 * and copy it to the File System
199 *
200 * @return boolean true if file is present in the DB, false otherwise
201 */
202 private function _checkFileInDB()
203 {
204 global $zdb;
205
206 try {
207 $select = $this->getCheckFileQuery();
208 $pic = $zdb->db->fetchRow($select);
209 //what's $pic if no result?
210 if ( $pic !== false ) {
211 // we must regenerate the picture file
212 $file_wo_ext = $this->store_path . $this->id;
213 $f = fopen($file_wo_ext . '.' . $pic->format, 'wb');
214 fwrite($f, $pic->picture);
215 fclose($f);
216 $this->format = $pic->format;
217 switch($this->format) {
218 case 'jpg':
219 $this->mime = 'image/jpeg';
220 break;
221 case 'png':
222 $this->mime = 'image/png';
223 break;
224 case 'gif':
225 $this->mime = 'image/gif';
226 break;
227 }
228 $this->file_path = $file_wo_ext . '.' . $this->format;
229 return true;
230 }
231 } catch (\Exception $e) {
232 return false;
233 }
234 }
235
236 /**
237 * Returns the relevant query to check if picture exists in database.
238 *
239 * @return string SELECT query
240 */
241 protected function getCheckFileQuery()
242 {
243 global $zdb;
244 $class = get_class($this);
245
246 $select = new \Zend_Db_Select($zdb->db);
247 $select->from(
248 array(PREFIX_DB . $this->tbl_prefix . $class::TABLE),
249 array(
250 'picture',
251 'format'
252 )
253 );
254 $select->where($class::PK . ' = ?', $this->db_id);
255 return $select;
256 }
257
258 /**
259 * Gets the default picture to show, anyways
260 *
261 * @return void
262 */
263 protected function getDefaultPicture()
264 {
265 $this->file_path = _CURRENT_TEMPLATE_PATH . 'images/default.png';
266 $this->format = 'png';
267 $this->mime = 'image/png';
268 $this->has_picture = false;
269 }
270
271 /**
272 * Set picture sizes
273 *
274 * @return void
275 */
276 private function _setSizes()
277 {
278 list($width, $height) = getimagesize($this->file_path);
279 $this->height = $height;
280 $this->width = $width;
281 $this->optimal_height = $height;
282 $this->optimal_width = $width;
283
284 if ($this->height > $this->width) {
285 if ($this->height > $this->max_height) {
286 $ratio = $this->max_height / $this->height;
287 $this->optimal_height = $this->max_height;
288 $this->optimal_width = $this->width * $ratio;
289 }
290 } else {
291 if ($this->width > $this->max_width) {
292 $ratio = $this->max_width / $this->width;
293 $this->optimal_width = $this->max_width;
294 $this->optimal_height = $this->height * $ratio;
295 }
296 }
297 }
298
299 /**
300 * Set header and displays the picture.
301 *
302 * @return object the binary file
303 */
304 public function display()
305 {
306 header('Content-type: '.$this->mime);
307 header('Content-Length: ' . filesize($this->file_path));
308 ob_clean();
309 flush();
310 readfile($this->file_path);
311 }
312
313 /**
314 * Deletes a picture, from both database and filesystem
315 *
316 * @param boolean $transaction Whether to use a transaction here or not
317 *
318 * @return boolean true if image was successfully deleted, false otherwise
319 */
320 public function delete($transaction = true)
321 {
322 global $zdb, $log;
323 $class = get_class($this);
324
325 try {
326 if ( $transaction === true ) {
327 $zdb->db->beginTransaction();
328 }
329 $del = $zdb->db->delete(
330 PREFIX_DB . $this->tbl_prefix . $class::TABLE,
331 $zdb->db->quoteInto($class::PK . ' = ?', $this->db_id)
332 );
333 if ( $del > 0 ) {
334 $file_wo_ext = $this->store_path . $this->id;
335
336 // take back default picture
337 $this->getDefaultPicture();
338 // fix sizes
339 $this->_setSizes();
340
341 $success = false;
342 $_file = null;
343 if ( file_exists($file_wo_ext . '.jpg') ) {
344 //return unlink($file_wo_ext . '.jpg');
345 $_file = $file_wo_ext . '.jpg';
346 $success = unlink($_file);
347 } elseif ( file_exists($file_wo_ext . '.png') ) {
348 //return unlink($file_wo_ext . '.png');
349 $_file = $file_wo_ext . '.png';
350 $success = unlink($_file);
351 } elseif ( file_exists($file_wo_ext . '.gif') ) {
352 //return unlink($file_wo_ext . '.gif');
353 $_file = $file_wo_ext . '.gif';
354 $success = unlink($_file);
355 }
356
357 if ( $_file !== null && $success !== true ) {
358 //unable to remove file that exists!
359 if ( $transaction === true ) {
360 $zdb->db->rollBack();
361 }
362 $log->log(
363 'The file ' . $_file . ' was found on the disk but cannot be removed.',
364 KLogger::ERR
365 );
366 return false;
367 } else {
368 if ( $transaction === true ) {
369 $zdb->db->commit();
370 }
371 return true;
372 }
373 } else {
374 if ( $transaction === true ) {
375 //properly ends transaction
376 $zdb->db->rollBack();
377 }
378 return false;
379 }
380
381
382 } catch (\Exception $e) {
383 if ( $transaction === true ) {
384 $zdb->db->rollBack();
385 }
386 $log->log(
387 'An error occured attempting to delete picture ' . $this->db_id .
388 'from database | ' . $e->getMessage(),
389 KLogger::ERR
390 );
391 return false;
392 }
393 }
394
395 /**
396 * Stores an image on the disk and in the database
397 *
398 * @param object $file the uploaded file
399 * @param boolean $ajax If the image cames from an ajax call (dnd)
400 *
401 * @return true|false result of the storage process
402 */
403 public function store($file, $ajax = false)
404 {
405 /** TODO:
406 - fix max size (by preferences ?)
407 - make possible to store images in database, filesystem or both
408 */
409 global $zdb, $log;
410
411 $class = get_class($this);
412
413 $name = $file['name'];
414 $tmpfile = $file['tmp_name'];
415
416 //First, does the file have a valid name?
417 $reg = "/^(.[^" . implode('', $this->_bad_chars) . "]+)\.(" .
418 implode('|', $this->_allowed_extensions) . ")$/i";
419 if ( preg_match($reg, $name, $matches) ) {
420 $log->log(
421 '[' . $class . '] Filename and extension are OK, proceed.',
422 KLogger::DEBUG
423 );
424 $extension = $matches[2];
425 if ( $extension == 'jpeg' ) {
426 //jpeg is an allowed extension,
427 //but we change it to jpg to reduce further tests :)
428 $extension = 'jpg';
429 }
430 } else {
431 $erreg = "/^(.[^" . implode('', $this->_bad_chars) . "]+)\.(.*)/i";
432 $m = preg_match($erreg, $name, $errmatches);
433
434 $err_msg = '[' . $class . '] ';
435 if ( $m == 1 ) {
436 //ok, we got a good filename and an extension. Extension is bad :)
437 $err_msg .= 'Invalid extension for file ' . $name . '.';
438 $ret = self::INVALID_EXTENSION;
439 } else {
440 $err_msg = 'Invalid filename `' . $name . '` (Tip: ';
441 $err_msg .= preg_replace(
442 '|%s|',
443 htmlentities($this->getbadChars()),
444 "file name should not contain any of: %s). "
445 );
446 $ret = self::INVALID_FILE;
447 }
448
449 $log->log(
450 $err_msg,
451 KLogger::ERR
452 );
453 return $ret;
454 }
455
456 //Second, let's check file size
457 if ( $file['size'] > ( $class::MAX_FILE_SIZE * 1024 ) ) {
458 $log->log(
459 '[' . $class . '] File is too big (' . ( $file['size'] * 1024 ) .
460 'Ko for maximum authorized ' . ( $class::MAX_FILE_SIZE * 1024 ) .
461 'Ko',
462 KLogger::ERR
463 );
464 return self::FILE_TOO_BIG;
465 } else {
466 $log->log('[' . $class . '] Filesize is OK, proceed', KLogger::DEBUG);
467 }
468
469 $current = getimagesize($tmpfile);
470
471 if ( !in_array($current['mime'], $this->_allowed_mimes) ) {
472 $log->log(
473 '[' . $class . '] Mimetype `' . $current['mime'] . '` not allowed',
474 KLogger::ERR
475 );
476 return self::MIME_NOT_ALLOWED;
477 } else {
478 $log->log(
479 '[' . $class . '] Mimetype is allowed, proceed',
480 KLogger::DEBUG
481 );
482 }
483
484 $this->delete();
485
486 $new_file = $this->store_path .
487 $this->id . '.' . $extension;
488 if ( $ajax === true ) {
489 rename($tmpfile, $new_file);
490 } else {
491 move_uploaded_file($tmpfile, $new_file);
492 }
493
494 // current[0] gives width ; current[1] gives height
495 if ( $current[0] > $this->max_width || $current[1] > $this->max_height ) {
496 /** FIXME: what if image cannot be resized?
497 Should'nt we want to stop the process here? */
498 $this->_resizeImage($new_file, $extension);
499 }
500
501 //store file in database
502 $f = fopen($new_file, 'r');
503 $picture = '';
504 while ( $r=fread($f, 8192) ) {
505 $picture .= $r;
506 }
507 fclose($f);
508
509 try {
510 $stmt = $zdb->db->prepare(
511 'INSERT INTO ' . PREFIX_DB .
512 $this->tbl_prefix . $class::TABLE . ' (' . $class::PK .
513 ', picture, format) VALUES (:id, :picture, :format)'
514 );
515
516 $stmt->bindParam('id', $this->db_id);
517 $stmt->bindParam('picture', $picture, \PDO::PARAM_LOB);
518 $stmt->bindParam('format', $extension);
519 $stmt->execute();
520 } catch (\Exception $e) {
521 /** FIXME */
522 $log->log(
523 'An error occured storing picture in database: ' .
524 $e->getMessage(),
525 KLogger::ERR
526 );
527 return self::SQL_ERROR;
528 }
529
530 return true;
531 }
532
533 /**
534 * Resize the image if it exceed max allowed sizes
535 *
536 * @param string $source the source image
537 * @param string $ext file's extension
538 * @param string $dest the destination image.
539 * If null, we'll use the source image. Defaults to null
540 *
541 * @return void
542 */
543 private function _resizeImage($source, $ext, $dest = null)
544 {
545 global $log;
546 $class = get_class($this);
547
548 if (function_exists("gd_info")) {
549 $gdinfo = gd_info();
550 $h = $this->max_height;
551 $w = $this->max_width;
552 if ( $dest == null ) {
553 $dest = $source;
554 }
555
556 switch(strtolower($ext)) {
557 case 'jpg':
558 if (!$gdinfo['JPEG Support']) {
559 $log->log(
560 '[' . $class . '] GD has no JPEG Support - ' .
561 'pictures could not be resized!',
562 KLogger::ERR
563 );
564 return false;
565 }
566 break;
567 case 'png':
568 if (!$gdinfo['PNG Support']) {
569 $log->log(
570 '[' . $class . '] GD has no PNG Support - ' .
571 'pictures could not be resized!',
572 KLogger::ERR
573 );
574 return false;
575 }
576 break;
577 case 'gif':
578 if (!$gdinfo['GIF Create Support']) {
579 $log->log(
580 '[' . $class . '] GD has no GIF Support - ' .
581 'pictures could not be resized!',
582 KLogger::ERR
583 );
584 return false;
585 }
586 break;
587 default:
588 return false;
589 }
590
591 list($cur_width, $cur_height, $cur_type, $curattr)
592 = getimagesize($source);
593
594 $ratio = $cur_width / $cur_height;
595
596 // calculate image size according to ratio
597 if ($cur_width>$cur_height) {
598 $h = $w/$ratio;
599 } else {
600 $w = $h*$ratio;
601 }
602
603 $thumb = imagecreatetruecolor($w, $h);
604 switch($ext) {
605 case 'jpg':
606 $image = ImageCreateFromJpeg($source);
607 imagecopyresampled(
608 $thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height
609 );
610 imagejpeg($thumb, $dest);
611 break;
612 case 'png':
613 $image = ImageCreateFromPng($source);
614 // Turn off alpha blending and set alpha flag. That prevent alpha
615 // transparency to be saved as an arbitrary color (black in my tests)
616 imagealphablending($thumb, false);
617 imagealphablending($image, false);
618 imagesavealpha($thumb, true);
619 imagesavealpha($image, true);
620 imagecopyresampled(
621 $thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height
622 );
623 imagepng($thumb, $dest);
624 break;
625 case 'gif':
626 $image = ImageCreateFromGif($source);
627 imagecopyresampled(
628 $thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height
629 );
630 imagegif($thumb, $dest);
631 break;
632 }
633 } else {
634 $log->log(
635 '[' . $class . '] GD is not present - ' .
636 'pictures could not be resized!',
637 KLogger::ERR
638 );
639 }
640 }
641
642 /* GETTERS */
643 /**
644 * Returns current file optimal height (resized)
645 *
646 * @return int optimal height
647 */
648 public function getOptimalHeight()
649 {
650 return round($this->optimal_height);
651 }
652
653 /**
654 * Returns current file height
655 *
656 * @return int current height
657 */
658 public function getHeight()
659 {
660 return $this->height;
661 }
662
663 /**
664 * Returns current file optimal width (resized)
665 *
666 * @return int optimal width
667 */
668 public function getOptimalWidth()
669 {
670 return $this->optimal_width;
671 }
672
673 /**
674 * Returns current file width
675 *
676 * @return int current width
677 */
678 public function getWidth()
679 {
680 return $this->width;
681 }
682
683 /**
684 * Returns current file format
685 *
686 * @return string
687 */
688 public function getFormat()
689 {
690 return $this->format;
691 }
692
693 /**
694 * Have we got a picture ?
695 *
696 * @return bool True if a picture matches adherent's id, false otherwise
697 */
698 public function hasPicture()
699 {
700 return $this->has_picture;
701 }
702
703 /**
704 * Returns unauthorized characters litteral values quoted, comma separated values
705 *
706 * @return string comma separated disallowed characters
707 */
708 public function getBadChars()
709 {
710 $ret = '';
711 foreach ( $this->_bad_chars as $char=>$regchar ) {
712 $ret .= '`' . $char . '`, ';
713 }
714 return $ret;
715 }
716
717 /**
718 * Returns allowed extensions
719 *
720 * @return string comma separated allowed extensiosn
721 */
722 public function getAllowedExts()
723 {
724 return implode(', ', $this->_allowed_extensions);
725 }
726
727 /**
728 * Return the array of allowed mime types
729 *
730 * @return array
731 */
732 public function getAllowedMimeTypes()
733 {
734 return $this->_allowed_mimes;
735 }
736
737 /**
738 * Returns current file full path
739 *
740 * @return string full file path
741 */
742 public function getPath()
743 {
744 return $this->file_path;
745 }
746
747 /**
748 * Returns current mime type
749 *
750 * @return string
751 */
752 public function getMime()
753 {
754 return $this->mime;
755 }
756
757 /**
758 * Return textual error message
759 *
760 * @param int $code The error code
761 *
762 * @return string Localized message
763 */
764 public function getErrorMessage($code)
765 {
766 $error = _T("An error occued.");
767 switch( $code ) {
768 case self::INVALID_FILE:
769 $error = _T("File name is invalid, it should not contain any special character or space.");
770 break;
771 case self::INVALID_EXTENSION:
772 $error = preg_replace(
773 '|%s|',
774 $this->getAllowedExts(),
775 _T("- File extension is not allowed, only %s files are.")
776 );
777 break;
778 case self::FILE_TOO_BIG:
779 $error = preg_replace(
780 '|%d|',
781 self::MAX_FILE_SIZE,
782 _T("File is too big. Maximum allowed size is %d")
783 );
784 break;
785 case self::MIME_NOT_ALLOWED:
786 /** FIXME: should be more descriptive */
787 $error = _T("Mime-Type not allowed");
788 break;
789 case self::SQL_ERROR:
790 case self::SQL_BLOB_ERROR:
791 $error = _T("An SQL error has occured.");
792 break;
793
794 }
795 return $error;
796 }
797
798 /**
799 * Return textual erro rmessage send by PHP after upload attempt
800 *
801 * @param int $error_code The error code
802 *
803 * @return string Localized message
804 */
805 public function getPhpErrorMessage($error_code)
806 {
807 switch ($error_code) {
808 case UPLOAD_ERR_INI_SIZE:
809 return _T("The uploaded file exceeds the upload_max_filesize directive in php.ini");
810 case UPLOAD_ERR_FORM_SIZE:
811 return _T("The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form");
812 case UPLOAD_ERR_PARTIAL:
813 return _T("The uploaded file was only partially uploaded");
814 case UPLOAD_ERR_NO_FILE:
815 return _T("No file was uploaded");
816 case UPLOAD_ERR_NO_TMP_DIR:
817 return _T("Missing a temporary folder");
818 case UPLOAD_ERR_CANT_WRITE:
819 return _T("Failed to write file to disk");
820 case UPLOAD_ERR_EXTENSION:
821 return _T("File upload stopped by extension");
822 default:
823 return _T("Unknown upload error");
824 }
825 }
826 }
827 ?>