3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2006-2014 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
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.
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.
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/>.
30 * @author Frédéric Jaqcuot <unknown@unknow.com>
31 * @author Johan Cwiklinski <johan@x-tnd.be>
32 * @copyright 2006-2014 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
35 * @link http://galette.tuxfamily.org
38 namespace Galette\Core
;
41 use Galette\Entity\Adherent
;
42 use Galette\Repository\Members
;
43 use Galette\IO\FileInterface
;
44 use Galette\IO\FileTrait
;
52 * @author Frédéric Jaqcuot <unknown@unknow.com>
53 * @author Johan Cwiklinski <johan@x-tnd.be>
54 * @copyright 2006-2014 The Galette Team
55 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
56 * @link http://galette.tuxfamily.org
58 class Picture
implements FileInterface
62 //constants that will not be overrided
63 const SQL_ERROR
= -10;
64 const SQL_BLOB_ERROR
= -11;
65 //constants that can be overrided
66 //(do not use self::CONSTANT, but get_class[$this]::CONSTANT)
67 const TABLE
= 'pictures';
68 const PK
= Adherent
::PK
;
70 protected $tbl_prefix = '';
75 protected $optimal_height;
76 protected $optimal_width;
80 protected $has_picture = false;
81 protected $store_path = GALETTE_PHOTOS_PATH
;
82 protected $max_width = 200;
83 protected $max_height = 200;
87 * Default constructor.
89 * @param int $id_adh the id of the member
91 public function __construct($id_adh = '')
96 array('jpeg', 'jpg', 'png', 'gif'),
98 'jpg' => 'image/jpeg',
104 // '!==' needed, otherwise ''==0
105 if ($id_adh !== '' && $id_adh !== null) {
107 if (!isset($this->db_id
)) {
108 $this->db_id
= $id_adh;
111 //if file does not exists on the FileSystem, check for it in the database
112 if (!$this->checkFileOnFS()) {
113 if ($this->checkFileInDB()) {
114 $this->has_picture
= true;
117 $this->has_picture
= true;
121 // if we still have no picture, take the default one
122 if (empty($this->file_path
)) {
123 $this->getDefaultPicture();
126 //we should not have an empty file_path, but...
127 if (!empty($this->file_path
)) {
133 * "Magic" function called on unserialize
137 public function __wakeup()
139 //if file has been deleted since we store our object in the session,
140 //we try to retrieve it
141 if (!$this->checkFileOnFS()) {
142 //if file does not exists on the FileSystem,
143 //check for it in the database
144 //$this->checkFileInDB();
146 $this->has_picture
= false;
149 // if we still have no picture, take the default one
150 if (empty($this->file_path
)) {
151 $this->getDefaultPicture();
154 //we should not have an empty file_path, but...
155 if (!empty($this->file_path
)) {
161 * Check if current file is present on the File System
163 * @return boolean true if file is present on FS, false otherwise
165 private function checkFileOnFS()
167 $file_wo_ext = $this->store_path
. $this->id
;
168 if (file_exists($file_wo_ext . '.jpg')) {
169 $this->file_path
= realpath($file_wo_ext . '.jpg');
170 $this->format
= 'jpg';
171 $this->mime
= 'image/jpeg';
173 } elseif (file_exists($file_wo_ext . '.png')) {
174 $this->file_path
= realpath($file_wo_ext . '.png');
175 $this->format
= 'png';
176 $this->mime
= 'image/png';
178 } elseif (file_exists($file_wo_ext . '.gif')) {
179 $this->file_path
= realpath($file_wo_ext . '.gif');
180 $this->format
= 'gif';
181 $this->mime
= 'image/gif';
188 * Check if current file is present in the database,
189 * and copy it to the File System
191 * @return boolean true if file is present in the DB, false otherwise
193 private function checkFileInDB()
198 $select = $this->getCheckFileQuery();
199 $results = $zdb->execute($select);
200 $pic = $results->current();
203 // we must regenerate the picture file
204 $file_wo_ext = $this->store_path
. $this->id
;
206 $file_wo_ext . '.' . $pic->format
,
210 $this->format
= $pic->format
;
211 switch ($this->format
) {
213 $this->mime
= 'image/jpeg';
216 $this->mime
= 'image/png';
219 $this->mime
= 'image/gif';
222 $this->file_path
= realpath($file_wo_ext . '.' . $this->format
);
225 } catch (\Exception
$e) {
231 * Returns the relevant query to check if picture exists in database.
233 * @return string SELECT query
235 protected function getCheckFileQuery()
238 $class = get_class($this);
240 $select = $zdb->select($this->tbl_prefix
. $class::TABLE
);
247 $select->where(array($class::PK
=> $this->db_id
));
252 * Gets the default picture to show, anyways
256 protected function getDefaultPicture()
258 $this->file_path
= realpath(_CURRENT_THEME_PATH
. 'images/default.png');
259 $this->format
= 'png';
260 $this->mime
= 'image/png';
261 $this->has_picture
= false;
269 private function setSizes()
271 list($width, $height) = getimagesize($this->file_path
);
272 $this->height
= $height;
273 $this->width
= $width;
274 $this->optimal_height
= $height;
275 $this->optimal_width
= $width;
277 if ($this->height
> $this->width
) {
278 if ($this->height
> $this->max_height
) {
279 $ratio = $this->max_height
/ $this->height
;
280 $this->optimal_height
= $this->max_height
;
281 $this->optimal_width
= $this->width
* $ratio;
284 if ($this->width
> $this->max_width
) {
285 $ratio = $this->max_width
/ $this->width
;
286 $this->optimal_width
= $this->max_width
;
287 $this->optimal_height
= $this->height
* $ratio;
293 * Set header and displays the picture.
295 * @return object the binary file
297 public function display()
299 header('Content-type: '.$this->mime
);
302 readfile($this->file_path
);
306 * Deletes a picture, from both database and filesystem
308 * @param boolean $transaction Whether to use a transaction here or not
310 * @return boolean true if image was successfully deleted, false otherwise
312 public function delete($transaction = true)
315 $class = get_class($this);
318 if ($transaction === true) {
319 $zdb->connection
->beginTransaction();
322 $delete = $zdb->delete($this->tbl_prefix
. $class::TABLE
);
324 $class::PK
. ' = ' . $this->db_id
326 $del = $zdb->execute($delete);
328 if (!$del->count() > 0) {
330 'Unable to remove picture database entry for ' . $this->db_id
,
333 //it may be possible image is missing in the database.
334 //let's try to remove file anyway.
337 $file_wo_ext = $this->store_path
. $this->id
;
339 // take back default picture
340 $this->getDefaultPicture();
346 if (file_exists($file_wo_ext . '.jpg')) {
347 //return unlink($file_wo_ext . '.jpg');
348 $_file = $file_wo_ext . '.jpg';
349 $success = unlink($_file);
350 } elseif (file_exists($file_wo_ext . '.png')) {
351 //return unlink($file_wo_ext . '.png');
352 $_file = $file_wo_ext . '.png';
353 $success = unlink($_file);
354 } elseif (file_exists($file_wo_ext . '.gif')) {
355 //return unlink($file_wo_ext . '.gif');
356 $_file = $file_wo_ext . '.gif';
357 $success = unlink($_file);
360 if ($_file !== null && $success !== true) {
361 //unable to remove file that exists!
362 if ($transaction === true) {
363 $zdb->connection
->rollBack();
366 'The file ' . $_file .
367 ' was found on the disk but cannot be removed.',
372 if ($transaction === true) {
373 $zdb->connection
->commit();
375 $this->has_picture
= false;
378 } catch (\Exception
$e) {
379 if ($transaction === true) {
380 $zdb->connection
->rollBack();
383 'An error occurred attempting to delete picture ' . $this->db_id
.
384 'from database | ' . $e->getMessage(),
392 * Stores an image on the disk and in the database
394 * @param object $file the uploaded file
395 * @param boolean $ajax If the image cames from an ajax call (dnd)
397 * @return true|false result of the storage process
399 public function store($file, $ajax = false)
401 /** TODO: fix max size (by preferences ?) */
404 $class = get_class($this);
406 $name = $file['name'];
407 $tmpfile = $file['tmp_name'];
409 //First, does the file have a valid name?
410 $reg = "/^([^" . implode('', $this->bad_chars
) . "]+)\.(" .
411 implode('|', $this->allowed_extensions
) . ")$/i";
412 if (preg_match($reg, $name, $matches)) {
414 '[' . $class . '] Filename and extension are OK, proceed.',
417 $extension = strtolower($matches[2]);
418 if ($extension == 'jpeg') {
419 //jpeg is an allowed extension,
420 //but we change it to jpg to reduce further tests :)
424 $erreg = "/^([^" . implode('', $this->bad_chars
) . "]+)\.(.*)/i";
425 $m = preg_match($erreg, $name, $errmatches);
427 $err_msg = '[' . $class . '] ';
429 //ok, we got a good filename and an extension. Extension is bad :)
430 $err_msg .= 'Invalid extension for file ' . $name . '.';
431 $ret = self
::INVALID_EXTENSION
;
433 $err_msg = 'Invalid filename `' . $name . '` (Tip: ';
434 $err_msg .= preg_replace(
436 htmlentities($this->getBadChars()),
437 "file name should not contain any of: %s). "
439 $ret = self
::INVALID_FILENAME
;
449 //Second, let's check file size
450 if ($file['size'] > ( $this->maxlenght
* 1024 )) {
452 '[' . $class . '] File is too big (' . ( $file['size'] * 1024 ) .
453 'Ko for maximum authorized ' . ( $this->maxlenght
* 1024 ) .
457 return self
::FILE_TOO_BIG
;
459 Analog
::log('[' . $class . '] Filesize is OK, proceed', Analog
::DEBUG
);
462 $current = getimagesize($tmpfile);
464 if (!in_array($current['mime'], $this->allowed_mimes
)) {
466 '[' . $class . '] Mimetype `' . $current['mime'] . '` not allowed',
469 return self
::MIME_NOT_ALLOWED
;
472 '[' . $class . '] Mimetype is allowed, proceed',
479 $new_file = $this->store_path
.
480 $this->id
. '.' . $extension;
481 if ($ajax === true) {
482 rename($tmpfile, $new_file);
484 move_uploaded_file($tmpfile, $new_file);
487 // current[0] gives width ; current[1] gives height
488 if ($current[0] > $this->max_width ||
$current[1] > $this->max_height
) {
489 /** FIXME: what if image cannot be resized?
490 Should'nt we want to stop the process here? */
491 $this->resizeImage($new_file, $extension);
494 return $this->storeInDb($zdb, $this->db_id
, $new_file, $extension);
498 * Stores an image in the database
500 * @param Db $zdb Database instance
501 * @param int $id Member ID
502 * @param string $file File path on disk
503 * @param string $ext File extension
507 private function storeInDb(Db
$zdb, $id, $file, $ext)
509 $f = fopen($file, 'r');
511 while ($r=fread($f, 8192)) {
516 $class = get_class($this);
519 $zdb->connection
->beginTransaction();
520 $stmt = $this->insert_stmt
;
522 $insert = $zdb->insert($this->tbl_prefix
. $class::TABLE
);
526 'picture' => ':picture',
527 'format' => ':format'
530 $stmt = $zdb->sql
->prepareStatementForSqlObject($insert);
531 $container = $stmt->getParameterContainer();
532 $container->offsetSet(
536 $container->offsetSet(
541 $container->offsetSet(
545 $stmt->setParameterContainer($container);
546 $this->insert_stmt
= $stmt;
552 'picture' => $picture,
556 $zdb->connection
->commit();
557 $this->has_picture
= true;
558 } catch (\Exception
$e) {
559 $zdb->connection
->rollBack();
561 'An error occurred storing picture in database: ' .
565 return self
::SQL_ERROR
;
572 * Check for missing images in database
574 * @param Db $zdb Database instance
578 public function missingInDb(Db
$zdb)
580 $existing_disk = array();
582 //retrieve files on disk
583 if ($handle = opendir($this->store_path
)) {
584 while (false !== ($entry = readdir($handle))) {
585 $reg = "/^(\d+)\.(" .
586 implode('|', $this->allowed_extensions
) . ")$/i";
587 if (preg_match($reg, $entry, $matches)) {
589 $extension = strtolower($matches[2]);
590 if ($extension == 'jpeg') {
591 //jpeg is an allowed extension,
592 //but we change it to jpg to reduce further tests :)
595 $existing_disk[$id] = array(
604 if (count($existing_disk) === 0) {
605 //no image on disk, nothing to do :)
609 //retrieve files in database
610 $class = get_class($this);
611 $select = $zdb->select($this->tbl_prefix
. $class::TABLE
);
613 ->columns(array($class::PK
))
614 ->where
->in($class::PK
, array_keys($existing_disk));
616 $results = $zdb->execute($select);
618 $existing_db = array();
619 foreach ($results as $result) {
620 $existing_db[] = (int)$result[self
::PK
];
623 $existing_diff = array_diff(array_keys($existing_disk), $existing_db);
625 //retrieve valid members ids
626 $members = new Members();
627 $valids = $members->getArrayList(
635 foreach ($valids as $valid) {
636 $file = $existing_disk[$valid->id_adh
];
640 $this->store_path
. $file['id'] . '.' . $file['ext'],
646 'Something went wrong opening images directory ' .
654 * Resize the image if it exceed max allowed sizes
656 * @param string $source the source image
657 * @param string $ext file's extension
658 * @param string $dest the destination image.
659 * If null, we'll use the source image. Defaults to null
663 private function resizeImage($source, $ext, $dest = null)
665 $class = get_class($this);
667 if (function_exists("gd_info")) {
669 $h = $this->max_height
;
670 $w = $this->max_width
;
675 switch (strtolower($ext)) {
677 if (!$gdinfo['JPEG Support']) {
679 '[' . $class . '] GD has no JPEG Support - ' .
680 'pictures could not be resized!',
687 if (!$gdinfo['PNG Support']) {
689 '[' . $class . '] GD has no PNG Support - ' .
690 'pictures could not be resized!',
697 if (!$gdinfo['GIF Create Support']) {
699 '[' . $class . '] GD has no GIF Support - ' .
700 'pictures could not be resized!',
710 list($cur_width, $cur_height, $cur_type, $curattr)
711 = getimagesize($source);
713 $ratio = $cur_width / $cur_height;
715 // calculate image size according to ratio
716 if ($cur_width>$cur_height) {
722 $thumb = imagecreatetruecolor($w, $h);
725 $image = ImageCreateFromJpeg($source);
726 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
727 imagejpeg($thumb, $dest);
730 $image = ImageCreateFromPng($source);
731 // Turn off alpha blending and set alpha flag. That prevent alpha
732 // transparency to be saved as an arbitrary color (black in my tests)
733 imagealphablending($thumb, false);
734 imagealphablending($image, false);
735 imagesavealpha($thumb, true);
736 imagesavealpha($image, true);
737 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
738 imagepng($thumb, $dest);
741 $image = ImageCreateFromGif($source);
742 imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
743 imagegif($thumb, $dest);
748 '[' . $class . '] GD is not present - ' .
749 'pictures could not be resized!',
756 * Returns current file optimal height (resized)
758 * @return int optimal height
760 public function getOptimalHeight()
762 return (int)round($this->optimal_height
, 1);
766 * Returns current file height
768 * @return int current height
770 public function getHeight()
772 return $this->height
;
776 * Returns current file optimal width (resized)
778 * @return int optimal width
780 public function getOptimalWidth()
782 return (int)round($this->optimal_width
, 1);
786 * Returns current file width
788 * @return int current width
790 public function getWidth()
796 * Returns current file format
800 public function getFormat()
802 return $this->format
;
806 * Have we got a picture ?
808 * @return bool True if a picture matches adherent's id, false otherwise
810 public function hasPicture()
812 return $this->has_picture
;
816 * Returns current file full path
818 * @return string full file path
820 public function getPath()
822 return $this->file_path
;
826 * Returns current mime type
830 public function getMime()
836 * Return textual error message
838 * @param int $code The error code
840 * @return string Localized message
842 public function getErrorMessage($code)
846 case self
::SQL_ERROR
:
847 case self
::SQL_BLOB_ERROR
:
848 $error = _T("An SQL error has occurred.");
852 if ($error === null) {
853 $error = $this->getErrorMessageFromCode($code);