]> git.agnieray.net Git - galette.git/blobdiff - galette/lib/Galette/Core/Mailing.php
Fix mailing preview; fixes #1800
[galette.git] / galette / lib / Galette / Core / Mailing.php
index e43ea12ccffbd0ff2e80efae0b695856c5f5967f..e2344014d58341e623a48eff892a3558d1337966 100644 (file)
@@ -7,7 +7,7 @@
  *
  * PHP version 5
  *
- * Copyright © 2009-2012 The Galette Team
+ * Copyright © 2009-2023 The Galette Team
  *
  * This file is part of Galette (http://galette.tuxfamily.org).
  *
  * @package   Galette
  *
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2009-2012 The Galette Team
+ * @copyright 2009-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
- * @version   SVN: $Id$
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.7dev - 2009-03-07
  */
 
 namespace Galette\Core;
 
-use Galette\Common\KLogger as KLogger;
+use Analog\Analog;
+use ArrayObject;
+use Galette\Entity\Adherent;
+use Galette\IO\File;
+use Laminas\Db\ResultSet\ResultSet;
 
 /**
  * Mailing features
@@ -46,55 +49,243 @@ use Galette\Common\KLogger as KLogger;
  * @name      Mailing
  * @package   Galette
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2009-2012 The Galette Team
+ * @copyright 2009-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.7dev - 2009-03-07
+ *
+ * @property string $subject
+ * @property string $message
+ * @property boolean $html
+ * @property integer $current_step
+ * @property-read  integer $step
+ * @property integer|string $id
+ * @property-read string $alt_message
+ * @property-read string $wrapped_message
+ * @property-read PHPMailer\PHPMailer\PHPMailer $mail
+ * @property-read PHPMailer\PHPMailer\PHPMailer $_mail
+ * @property-read array $errors
+ * @property-read array $recipients
+ * @property-read string|false $tmp_path
+ * @property array $attachments
+ * @property-read string $sender_name
+ * @property-read string $sender_address
+ * @property integer $history_id
  */
 class Mailing extends GaletteMail
 {
-    const STEP_START = 0;
-    const STEP_PREVIEW = 1;
-    const STEP_SEND = 2;
-    const STEP_SENT = 3;
+    public const STEP_START = 0;
+    public const STEP_PREVIEW = 1;
+    public const STEP_SEND = 2;
+    public const STEP_SENT = 3;
+
+    public const MIME_HTML = 'text/html';
+    public const MIME_TEXT = 'text/plain';
+    public const MIME_DEFAULT = self::MIME_TEXT;
 
-    const MIME_HTML = 'text/html';
-    const MIME_TEXT = 'text/plain';
-    const MIME_DEFAULT = self::MIME_TEXT;
+    private $id;
 
-    private $_unreachables;
-    private $_mrecipients;
-    private $_current_step;
+    private $unreachables = array();
+    private $mrecipients = array();
+    private $current_step;
 
-    private $_mime_type;
+    private $mime_type;
+
+    private $tmp_path;
+    private $history_id;
 
     /**
-    * Default constructor
-    *
-    * @param array $members An array of members
-    */
-    public function __construct($members)
+     * Default constructor
+     *
+     * @param Preferences $preferences Preferences instance
+     * @param array       $members     An array of members
+     * @param int         $id          Identifier, defaults to null
+     */
+    public function __construct(Preferences $preferences, array $members = [], int $id = null)
     {
-        $this->_current_step = self::STEP_START;
-        $this->_mime_type = self::MIME_DEFAULT;
+        parent::__construct($preferences);
+        $this->id = $id ?? $this->generateNewId();
+
+        $this->current_step = self::STEP_START;
+        $this->mime_type = self::MIME_DEFAULT;
         /** TODO: add a preference that propose default mime-type to use,
             then init it here */
-        if ( $members !== null) {
-            //Check which members have a valid email adress and which have not
+        if (count($members)) {
+            //Check which members have a valid email address and which have not
             $this->setRecipients($members);
         }
+        $this->loadAttachments();
+    }
+
+    /**
+     * Generate new mailing id and temporary path
+     *
+     * @return string
+     */
+    private function generateNewId(): string
+    {
+        $id = '';
+        $chars = 'abcdefghjkmnpqrstuvwxyz0123456789';
+        $i = 0;
+        $size = 30;
+        while ($i <= $size - 1) {
+            $num = mt_rand(0, strlen($chars) - 1) % strlen($chars);
+            $id .= substr($chars, $num, 1);
+            $i++;
+        }
+
+        $this->id = $id;
+        $this->generateTmpPath($this->id);
+        return $this->id;
+    }
+
+    /**
+     * Generate temporary path
+     *
+     * @param string $id Random id, defautls to null
+     *
+     * @return void
+     */
+    private function generateTmpPath($id = null)
+    {
+        if ($id === null) {
+            $id = $this->generateNewId();
+        }
+        $this->tmp_path = GALETTE_ATTACHMENTS_PATH . '/' . $id;
+    }
+
+    /**
+     * Load mailing attachments
+     *
+     * @return void
+     */
+    private function loadAttachments()
+    {
+        $dir = '';
+        if (
+            isset($this->tmp_path)
+            && trim($this->tmp_path) !== ''
+        ) {
+            $dir = $this->tmp_path;
+        } else {
+            $dir = GALETTE_ATTACHMENTS_PATH . $this->id . '/';
+        }
+
+        $files = glob($dir . '*.*');
+        foreach ($files as $file) {
+            $f = new File($dir);
+            $f->setFileName(str_replace($dir, '', $file));
+            $this->attachments[] = $f;
+        }
     }
 
     /**
-    * Apply final header to mail and send it :-)
-    *
-    * @return GaletteMail::MAIL_ERROR|GaletteMail::MAIL_SENT
-    */
+     * Loads a mailing from history
+     *
+     * @param ArrayObject $rs  Mailing entry
+     * @param boolean     $new True if we create a 'new' mailing,
+     *                         false otherwise (from preview for example)
+     *
+     * @return boolean
+     */
+    public function loadFromHistory(ArrayObject $rs, $new = true)
+    {
+        global $zdb;
+
+        try {
+            $orig_recipients = unserialize($rs->mailing_recipients);
+        } catch (\Throwable $e) {
+            Analog::log(
+                'Unable to unserialize recipients for mailing ' . $rs->mailing_id,
+                Analog::ERROR
+            );
+            $orig_recipients = [];
+        }
+
+        $_recipients = array();
+        $mdeps = ['parent' => true];
+        foreach ($orig_recipients as $k => $v) {
+            $m = new Adherent($zdb, $k, $mdeps);
+            $_recipients[] = $m;
+        }
+        $this->setRecipients($_recipients);
+        $this->subject = $rs->mailing_subject;
+        $this->message = $rs->mailing_body;
+        $this->html = $this->message != strip_tags($this->message) ? true : false;
+        if ($rs->mailing_sender_name !== null || $rs->mailing_sender_address !== null) {
+            $this->setSender(
+                $rs->mailing_sender_name,
+                $rs->mailing_sender_address
+            );
+        }
+        //if mailing has already been sent, generate a new id and copy attachments
+        if ($rs->mailing_sent && $new) {
+            $this->generateNewId();
+            $this->copyAttachments($rs->mailing_id);
+        } else {
+            $this->tmp_path = null;
+            $this->id = $rs->mailing_id;
+            if (!$this->attachments) {
+                $this->loadAttachments();
+            }
+            $this->history_id = $rs->mailing_id;
+        }
+        return true;
+    }
+
+    /**
+     * Copy attachments from another mailing
+     *
+     * @param int $id Original mailing id
+     *
+     * @return void
+     */
+    private function copyAttachments($id)
+    {
+        $source_dir = GALETTE_ATTACHMENTS_PATH . $id . '/';
+        $dest_dir = GALETTE_ATTACHMENTS_PATH . $this->id . '/';
+
+        if (file_exists($source_dir)) {
+            if (file_exists($dest_dir)) {
+                throw new \RuntimeException(
+                    str_replace(
+                        '%s',
+                        $this->id,
+                        'Attachments directory already exists for mailing %s!'
+                    )
+                );
+            } else {
+                //create directory
+                mkdir($dest_dir);
+                //copy attachments from source mailing and populate attachments
+                $this->attachments = array();
+                $files = glob($source_dir . '*.*');
+                foreach ($files as $file) {
+                    $f = new File($source_dir);
+                    $f->setFileName(str_replace($source_dir, '', $file));
+                    $f->copyTo($dest_dir);
+                    $this->attachments[] = $f;
+                }
+            }
+        } else {
+            Analog::log(
+                'No attachments in source directory',
+                Analog::DEBUG
+            );
+        }
+    }
+
+    /**
+     * Apply final header to email and send it :-)
+     *
+     * @return int
+     */
     public function send()
     {
         $m = array();
-        foreach ( $this->_mrecipients as $member ) {
-            $m[$member->email] = $member->sname;
+        foreach ($this->mrecipients as $member) {
+            $email = $member->getEmail();
+            $m[$email] = $member->sname;
         }
         parent::setRecipients($m);
         return parent::send();
@@ -103,150 +294,373 @@ class Mailing extends GaletteMail
     /**
      * Set mailing recipients
      *
-     * @param <type> $members Array of Adherent objects
+     * @param array $members Array of Adherent objects
      *
-     * @return void
+     * @return bool
      */
     public function setRecipients($members)
     {
         $m = array();
-        $this->_mrecipients = array();
-        $this->_unreachables = array();
+        $this->mrecipients = array();
+        $this->unreachables = array();
 
         foreach ($members as $member) {
-            if ( trim($member->email) != '' && self::isValidEmail($member->email) ) {
-                if ( !in_array($member, $this->_mrecipients) ) {
-                    $this->_mrecipients[] = $member;
+            $email = $member->getEmail();
+
+            if (trim($email) != '' && self::isValidEmail($email)) {
+                if (!in_array($member, $this->mrecipients)) {
+                    $this->mrecipients[] = $member;
                 }
-                $m[$member->email] = $member->sname;
+                $m[$email] = $member->sname;
             } else {
-                if ( !in_array($member, $this->_unreachables) ) {
-                    $this->_unreachables[] = $member;
+                if (!in_array($member, $this->unreachables)) {
+                    $this->unreachables[] = $member;
                 }
             }
         }
-        parent::setRecipients($m);
+        return parent::setRecipients($m);
+    }
+
+    /**
+     * Store maling attachments
+     *
+     * @param array $files Array of uploaded files to store
+     *
+     * @return true|int error code
+     */
+    public function store($files)
+    {
+        if ($this->tmp_path === null) {
+            $this->generateTmpPath();
+        }
+
+        if (!file_exists($this->tmp_path)) {
+            //directory does not exist, create it
+            mkdir($this->tmp_path);
+        }
+
+        if (!is_dir($this->tmp_path)) {
+            throw new \RuntimeException(
+                $this->tmp_path . ' should be a directory!'
+            );
+        }
+
+        //store files
+        $attachment = new File($this->tmp_path);
+        $res = $attachment->store($files);
+        if ($res < 0) {
+            return $res;
+        } else {
+            $this->attachments[] = $attachment;
+        }
+
+        return true;
+    }
+
+    /**
+     * Move attachments with final id once mailing has been stored
+     *
+     * @param int $id Mailing history id
+     *
+     * @return void
+     */
+    public function moveAttachments($id)
+    {
+        if (
+            isset($this->tmp_path)
+            && trim($this->tmp_path) !== ''
+            && count($this->attachments) > 0
+        ) {
+            foreach ($this->attachments as &$attachment) {
+                $old_path = $attachment->getDestDir() . $attachment->getFileName();
+                $new_path = GALETTE_ATTACHMENTS_PATH . $id . '/' .
+                    $attachment->getFileName();
+                if (!file_exists(GALETTE_ATTACHMENTS_PATH . $id)) {
+                    mkdir(GALETTE_ATTACHMENTS_PATH . $id);
+                }
+                $moved = rename($old_path, $new_path);
+                if ($moved) {
+                    $attachment->setDestDir(GALETTE_ATTACHMENTS_PATH);
+                }
+            }
+            rmdir($this->tmp_path);
+            $this->tmp_path = null;
+        }
+    }
+
+    /**
+     * Remove specified attachment
+     *
+     * @param string $name Filename
+     *
+     * @return void
+     */
+    public function removeAttachment($name)
+    {
+        $to_remove = null;
+        if (
+            isset($this->tmp_path)
+            && trim($this->tmp_path) !== ''
+            && file_exists($this->tmp_path)
+        ) {
+            $to_remove = $this->tmp_path;
+        } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
+            $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
+        }
+
+        if ($to_remove !== null) {
+            $to_remove .= '/' . $name;
+
+            if (!$this->attachments) {
+                $this->loadAttachments();
+            }
+
+            if (file_exists($to_remove)) {
+                $i = 0;
+                foreach ($this->attachments as $att) {
+                    if ($att->getFileName() == $name) {
+                        unset($this->attachments[$i]);
+                        unlink($to_remove);
+                        break;
+                    }
+                    $i++;
+                }
+            } else {
+                Analog::log(
+                    str_replace(
+                        '%file',
+                        $name,
+                        'File %file does not exists and cannot be removed!'
+                    ),
+                    Analog::WARNING
+                );
+            }
+        } else {
+            throw new \RuntimeException(
+                'Unable to get attachments path!'
+            );
+        }
+    }
+
+    /**
+     * Remove mailing attachments
+     *
+     * @param boolean $temp Remove only tmporary attachments,
+     *                      to avoid history breaking
+     *
+     * @return void|false
+     */
+    public function removeAttachments($temp = false)
+    {
+        $to_remove = null;
+        if (
+            isset($this->tmp_path)
+            && trim($this->tmp_path) !== ''
+            && file_exists($this->tmp_path)
+        ) {
+            $to_remove = $this->tmp_path;
+        } elseif (file_exists(GALETTE_ATTACHMENTS_PATH . $this->id)) {
+            if ($temp === true) {
+                return false;
+            }
+            $to_remove = GALETTE_ATTACHMENTS_PATH . $this->id;
+        }
+
+        if ($to_remove !== null) {
+            $rdi = new \RecursiveDirectoryIterator(
+                $to_remove,
+                \FilesystemIterator::SKIP_DOTS
+            );
+            $contents = new \RecursiveIteratorIterator(
+                $rdi,
+                \RecursiveIteratorIterator::CHILD_FIRST
+            );
+            foreach ($contents as $path) {
+                if ($path->isFile()) {
+                    unlink($path->getPathname());
+                } else {
+                    rmdir($path->getPathname());
+                }
+            }
+            rmdir($to_remove);
+        }
+    }
+
+    /**
+     * Return textual error message
+     *
+     * @param int $code The error code
+     *
+     * @return string Localized message
+     */
+    public function getAttachmentErrorMessage($code)
+    {
+        $f = new File($this->tmp_path);
+        return $f->getErrorMessage($code);
+    }
+
+    /**
+     * Does mailing already exists in history?
+     *
+     * @return boolean
+     */
+    public function existsInHistory()
+    {
+        return isset($this->history_id);
     }
 
     /**
-    * Global getter method
-    *
-    * @param string $name name of the property we want to retrive
-    *
-    * @return false|object the called property
-    */
+     * Global getter method
+     *
+     * @param string $name name of the property we want to retrieve
+     *
+     * @return mixed the called property
+     */
     public function __get($name)
     {
-        global $log;
         $forbidden = array('ordered');
-        if ( !in_array($name, $forbidden) ) {
-            switch($name) {
-            case 'alt_message':
-                return $this->cleanedHtml();
-                break;
-            case 'step':
-                return $this->current_step;
-                break;
-            case 'subject':
-                return $this->getSubject();
-                break;
-            case 'message':
-                return $this->getMessage();
-                break;
-            case 'html':
-                return $this->isHTML();
-                break;
-            case 'mail':
-            case '_mail':
-                return $this->getPhpMailer();
-                break;
-            case 'errors':
-                return $this->getErrors();
-                break;
-            case 'recipients':
-                return $this->_mrecipients;
-                break;
-            default:
-                $rname = '_' . $name;
-                $log->log(
-                    '[' . get_class($this) . 'Trying to get ' . $name .
-                    ' renamed: ' . $rname,
-                    KLogger::DEBUG
-                );
-                return $this->$rname;
-                break;
+        if (!in_array($name, $forbidden)) {
+            switch ($name) {
+                case 'alt_message':
+                    return $this->cleanedHtml();
+                case 'step':
+                    return $this->current_step;
+                case 'subject':
+                    return $this->getSubject();
+                case 'message':
+                    return $this->getMessage();
+                case 'wrapped_message':
+                    return $this->getWrappedMessage();
+                case 'html':
+                    return $this->isHTML();
+                case 'mail':
+                case '_mail':
+                    return $this->getPhpMailer();
+                case 'errors':
+                    return $this->getErrors();
+                case 'recipients':
+                    return $this->mrecipients;
+                case 'tmp_path':
+                    if (isset($this->tmp_path) && trim($this->tmp_path) !== '') {
+                        return $this->tmp_path;
+                    } else {
+                        //no attachments
+                        return false;
+                    }
+                case 'attachments':
+                    return $this->attachments;
+                case 'sender_name':
+                    return $this->getSenderName();
+                case 'sender_address':
+                    return $this->getSenderAddress();
+                case 'history_id':
+                    return $this->$name;
+                default:
+                    Analog::log(
+                        '[' . get_class($this) . 'Trying to get ' . $name,
+                        Analog::DEBUG
+                    );
+                    return $this->$name;
             }
         } else {
-            $log->log(
-                '[' . get_class($this) . 'Unable to get ' . $name .
-                ' renamed: ' . $rname,
-                KLogger::ERR
+            Analog::log(
+                '[' . get_class($this) . 'Unable to get ' . $name,
+                Analog::ERROR
             );
             return false;
         }
     }
 
     /**
-    * Global setter method
-    *
-    * @param string $name  name of the property we want to assign a value to
-    * @param object $value a relevant value for the property
-    *
-    * @return void
-    */
-    public function __set($name, $value)
+     * Global isset method
+     * Required for twig to access properties via __get
+     *
+     * @param string $name name of the property we want to retrieve
+     *
+     * @return bool
+     */
+    public function __isset($name)
     {
-        global $log;
-        $rname = '_' . $name;
-
-        switch( $name ) {
-        case 'subject':
-            $this->setSubject($value);
-            break;
-        case 'message':
-            $this->setMessage($value);
-            break;
-        case 'html':
-            if ( is_bool($value) ) {
-                $this->isHTML($value);
-            } else {
-                $log->log(
-                    '[' . get_class($this) . '] Value for field `' . $name .
-                    '` should be boolean - (' . gettype($value) . ')' .
-                    $value . ' given',
-                    KLogger::WARN
-                );
+        $forbidden = array('ordered');
+        if (!in_array($name, $forbidden)) {
+            switch ($name) {
+                case 'alt_message':
+                case 'step':
+                case 'subject':
+                case 'message':
+                case 'wrapped_message':
+                case 'html':
+                case 'mail':
+                case '_mail':
+                case 'errors':
+                case 'recipients':
+                case 'tmp_path':
+                case 'attachments':
+                case 'sender_name':
+                case 'sender_address':
+                    return true;
             }
-            break;
-        /** FIXME: remove... should no longer exists with phpMailer */
-        /*case 'message':
-            $this->$rname = (get_magic_quotes_gpc())
-                ? stripslashes($value)
-                : $value;
-            break;*/
-        case 'current_step':
-            if ( is_int($value)
-                && (   $value == self::STEP_START
-                || $value == self::STEP_PREVIEW
-                || $value == self::STEP_SEND
-                || $value == self::STEP_SENT )
-            ) {
-                $this->_current_step = (int)$value;
-            } else {
-                $log->log(
-                    '[' . get_class($this) . '] Value for field `' . $name .
-                    '` should be integer and know - (' . gettype($value) . ')' .
-                    $value . ' given',
-                    KLogger::WARN
+            return isset($this->$name);
+        }
+
+        return false;
+    }
+
+    /**
+     * Global setter method
+     *
+     * @param string $name  name of the property we want to assign a value to
+     * @param mixed  $value a relevant value for the property
+     *
+     * @return void
+     */
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            case 'subject':
+                $this->setSubject($value);
+                break;
+            case 'message':
+                $this->setMessage($value);
+                break;
+            case 'html':
+                if (is_bool($value)) {
+                    $this->isHTML($value);
+                } else {
+                    Analog::log(
+                        '[' . get_class($this) . '] Value for field `' . $name .
+                        '` should be boolean - (' . gettype($value) . ')' .
+                        $value . ' given',
+                        Analog::WARNING
+                    );
+                }
+                break;
+            case 'current_step':
+                if (
+                    is_int($value)
+                    && ($value == self::STEP_START
+                    || $value == self::STEP_PREVIEW
+                    || $value == self::STEP_SEND
+                    || $value == self::STEP_SENT)
+                ) {
+                    $this->current_step = (int)$value;
+                } else {
+                    Analog::log(
+                        '[' . get_class($this) . '] Value for field `' . $name .
+                        '` should be integer and know - (' . gettype($value) . ')' .
+                        $value . ' given',
+                        Analog::WARNING
+                    );
+                }
+                break;
+            case 'id':
+                $this->id = $value;
+                break;
+            default:
+                Analog::log(
+                    '[' . get_class($this) . '] Unable to set property `' . $name . '`',
+                    Analog::WARNING
                 );
-            }
-            break;
-        default:
-            $log->log(
-                '[' . get_class($this) . '] Unable to set proprety `' . $name . '`',
-                KLogger::WARN
-            );
-            break;
         }
     }
 }