<?php

/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Mail;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Event\Mail\BeforeRenderingMailTemplateEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Mail\Exception\MailDisabledException;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Path;
use Joomla\Registry\Registry;
use PHPMailer\PHPMailer\Exception as phpmailerException;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Email Templating Class
 *
 * @since  4.0.0
 */
class MailTemplate
{
    /**
     * Mailer object to send the actual mail.
     *
     * @var    \Joomla\CMS\Mail\Mail
     * @since  4.0.0
     */
    protected $mailer;

    /**
     * Identifier of the mail template.
     *
     * @var    string
     * @since  4.0.0
     */
    protected $template_id;

    /**
     * Language of the mail template.
     *
     * @var    string
     */
    protected $language;

    /**
     *
     * @var    string[]
     * @since  4.0.0
     */
    protected $data = [];

    /**
     *
     * @var    string[]
     * @since  5.1.0
     */
    protected $plain_data = [];

    /**
     *
     * @var    string[]
     * @since  5.1.3
     */
    protected $unsafe_tags = [];

    /**
     *
     * @var    string[]
     * @since  4.0.0
     */
    protected $attachments = [];

    /**
     * List of recipients of the email
     *
     * @var    \stdClass[]
     * @since  4.0.0
     */
    protected $recipients = [];

    /**
     * Reply To of the email
     *
     * @var    \stdClass
     * @since  4.0.0
     */
    protected $replyto;

    /**
     * Layout mailtemplate options of the email
     *
     * @var    string[]
     * @since  5.2.0
     */
    protected $layoutTemplateData = [];

    /**
     * Constructor for the mail templating class
     *
     * @param   string   $templateId  Id of the mail template.
     * @param   string   $language    Language of the template to use.
     * @param   ?Mail    $mailer      Mail object to send the mail with.
     *
     * @since   4.0.0
     */
    public function __construct($templateId, $language, ?Mail $mailer = null)
    {
        $this->template_id = $templateId;
        $this->language    = $language;

        if ($mailer) {
            $this->mailer = $mailer;
        } else {
            $this->mailer = Factory::getMailer();
        }
    }

    /**
     * Add an attachment to the mail
     *
     * @param   string  $name  Filename of the attachment
     * @param   string  $file  Either a filepath or filecontent
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function addAttachment($name, $file)
    {
        $attachment          = new \stdClass();
        $attachment->name    = $name;
        $attachment->file    = $file;
        $this->attachments[] = $attachment;
    }

    /**
     * Adds recipients for this mail
     *
     * @param   string  $mail  Mail address of the recipient
     * @param   string  $name  Name of the recipient
     * @param   string  $type  How should the recipient receive the mail? ('to', 'cc', 'bcc')
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function addRecipient($mail, $name = null, $type = 'to')
    {
        $recipient          = new \stdClass();
        $recipient->mail    = $mail;
        $recipient->name    = $name ?? $mail;
        $recipient->type    = $type;
        $this->recipients[] = $recipient;
    }

    /**
     * Set reply to for this mail
     *
     * @param   string  $mail  Mail address to reply to
     * @param   string  $name  Name
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function setReplyTo($mail, $name = '')
    {
        $reply         = new \stdClass();
        $reply->mail   = $mail;
        $reply->name   = $name;
        $this->replyto = $reply;
    }

    /**
     * Add data to the html layout template
     *
     * @param   array  $data  Associative array of strings for the layout
     *
     * @return  void
     *
     * @since   5.2.0
     */
    public function addLayoutTemplateData($data)
    {
        $this->layoutTemplateData = array_merge($this->layoutTemplateData, $data);
    }

    /**
     * Add data to replace in the template
     *
     * @param   array  $data  Associative array of strings to replace
     * @param   bool   $plain Only use the data for plain-text emails.
     *
     * @return  void
     *
     * @since   4.0.0
     */
    public function addTemplateData($data, $plain = false)
    {
        if (!$plain) {
            $this->data = array_merge($this->data, $data);
        } else {
            $this->plain_data = array_merge($this->plain_data, $data);
        }
    }

    /**
     * Mark tags as unsafe to ensure escaping in HTML mails
     *
     * @param   array   $tags  Tag names
     *
     * @return  void
     *
     * @since   5.1.3
     */
    public function addUnsafeTags($tags)
    {
        $this->unsafe_tags = array_merge($this->unsafe_tags, array_map('strtoupper', $tags));
    }

    /**
     * Render and send the mail
     *
     * @return  boolean  True on success
     *
     * @since   4.0.0
     * @throws  \Exception
     * @throws  MailDisabledException
     * @throws  phpmailerException
     */
    public function send()
    {
        $config = ComponentHelper::getParams('com_mails');

        $mail = self::getTemplate($this->template_id, $this->language);

        // If the Mail Template was not found in the db, we cannot send an email.
        if ($mail === null) {
            return false;
        }

        /** @var Registry $params */
        $params      = $mail->params;
        $app         = Factory::getApplication();
        $replyTo     = $app->get('replyto', '');
        $replyToName = $app->get('replytoname', '');

        if ((int) $config->get('alternative_mailconfig', 0) === 1 && (int) $params->get('alternative_mailconfig', 0) === 1) {
            if ($this->mailer->Mailer === 'smtp' || $params->get('mailer') === 'smtp') {
                $smtpauth   = ($params->get('smtpauth', $app->get('smtpauth')) == 0) ? null : 1;
                $smtpuser   = $params->get('smtpuser', $app->get('smtpuser'));
                $smtppass   = $params->get('smtppass', $app->get('smtppass'));
                $smtphost   = $params->get('smtphost', $app->get('smtphost'));
                $smtpsecure = $params->get('smtpsecure', $app->get('smtpsecure'));
                $smtpport   = $params->get('smtpport', $app->get('smtpport'));
                $this->mailer->useSmtp($smtpauth, $smtphost, $smtpuser, $smtppass, $smtpsecure, $smtpport);
            }

            if ($params->get('mailer') === 'sendmail') {
                $this->mailer->isSendmail();
            }

            $mailfrom = $params->get('mailfrom', $app->get('mailfrom'));
            $fromname = $params->get('fromname', $app->get('fromname'));

            if (MailHelper::isEmailAddress($mailfrom)) {
                $this->mailer->setFrom(MailHelper::cleanLine($mailfrom), MailHelper::cleanLine($fromname), false);
            }

            $replyTo     = $params->get('replyto', $replyTo);
            $replyToName = $params->get('replytoname', $replyToName);
        }

        $useLayout   = $config->get('disable_htmllayout', '1');

        if ((int) $config->get('alternative_mailconfig', 0) === 1) {
            $useLayout   = $params->get('disable_htmllayout', $useLayout);
        }

        $app->getDispatcher()->dispatch('onMailBeforeRendering', new BeforeRenderingMailTemplateEvent(
            'onMailBeforeRendering',
            ['templateId' => $this->template_id, 'subject' => $this]
        ));

        $subject = $this->replaceTags(Text::_($mail->subject), $this->data);
        $this->mailer->setSubject($subject);

        $mailStyle = $config->get('mail_style', 'plaintext');

        // Use the plain-text replacement data, if specified.
        $plainData = $this->plain_data ?: $this->data;
        $plainBody = $this->replaceTags(Text::_($mail->body), $plainData);
        $htmlBody  = $useLayout ? Text::_($mail->htmlbody) : $this->replaceTags(Text::_($mail->htmlbody), $this->data, true);

        if ($mailStyle === 'plaintext' || $mailStyle === 'both') {
            // If the Plain template is empty try to convert the HTML template to a Plain text
            if (!$plainBody) {
                $plainBody = strip_tags(str_replace(['<br>', '<br />', '<br/>'], "\n", $htmlBody));
            }

            $this->mailer->setBody($plainBody);

            // Set alt body, use $mailer->Body directly because it was filtered by $mailer->setBody()
            if ($mailStyle === 'both') {
                $this->mailer->AltBody = $this->mailer->Body;
            }
        }

        if ($mailStyle === 'html' || $mailStyle === 'both') {
            $this->mailer->isHtml(true);

            // If HTML body is empty try to convert the Plain template to html
            if (!$htmlBody) {
                $htmlBody = nl2br($this->replaceTags(Text::_($mail->body), $plainData, true), false);
            }

            if ($useLayout) {
                // Add additional data to the layout template
                $this->addLayoutTemplateData([
                    'siteName' => $app->get('sitename'),
                    'lang'     => substr($this->language, 0, 2),
                ]);

                $layout = $config->get('mail_htmllayout', 'mailtemplate');
                $logo   = (string) $config->get('mail_logofile', '');

                // Check alternative mailconfig
                if ((int) $config->get('alternative_mailconfig', 0) === 1) {
                    $layout = $params->get('htmllayout', $layout);
                    $logo   = $params->get('disable_logofile', 1) ? $logo : '' ;
                }

                // Add the logo to the mail as inline attachment
                if ($logo) {
                    $logo = Path::check(JPATH_ROOT . '/' . HTMLHelper::_('cleanImageURL', $logo)->url);
                    if (is_file(urldecode($logo))) {
                        # Attach the logo as inline attachment
                        $this->mailer->addAttachment($logo, 'site-logo', 'base64', mime_content_type($logo), 'inline');

                        // We need only the cid for attached logo file
                        $this->addLayoutTemplateData(['logo' => 'site-logo']);
                    }
                }

                // Check if layout is a template override
                $layoutParts = explode(':', $layout);

                if (\count($layoutParts) === 2) {
                    $layout = $layoutParts[1];
                }

                // Wrap the default Joomla mail template around the HTML body
                $layoutFile = new FileLayout('joomla.mail.' . $layout, null, ['client' => 'site']);

                // Set the template layout path if needed
                if (\count($layoutParts) === 2) {
                    $layoutFile->addIncludePaths([
                        JPATH_SITE . '/templates/' . $layoutParts[0] . '/html/layouts',
                        JPATH_SITE . '/templates/' . $layoutParts[0] . '/html/layouts/com_mails',
                    ]);
                }

                $htmlBody = $layoutFile->render(['mail' => $htmlBody, 'extra' => $this->layoutTemplateData], null);

                $htmlBody = $this->replaceTags(Text::_($htmlBody), $this->data);
            }

            $htmlBody = MailHelper::convertRelativeToAbsoluteUrls($htmlBody);

            $this->mailer->setBody($htmlBody);
        }

        if ($config->get('copy_mails') && $params->get('copyto')) {
            $this->mailer->addBcc($params->get('copyto'));
        }

        foreach ($this->recipients as $recipient) {
            switch ($recipient->type) {
                case 'cc':
                    $this->mailer->addCc($recipient->mail, $recipient->name);
                    break;
                case 'bcc':
                    $this->mailer->addBcc($recipient->mail, $recipient->name);
                    break;
                case 'to':
                default:
                    $this->mailer->addAddress($recipient->mail, $recipient->name);
            }
        }

        if ($this->replyto) {
            $this->mailer->addReplyTo($this->replyto->mail, $this->replyto->name);
        } elseif ($replyTo) {
            $this->mailer->addReplyTo($replyTo, $replyToName);
        }

        if (trim($config->get('attachment_folder', ''))) {
            $folderPath = rtrim(Path::check(JPATH_ROOT . '/' . $config->get('attachment_folder')), \DIRECTORY_SEPARATOR);

            if ($folderPath && $folderPath !== Path::clean(JPATH_ROOT) && is_dir($folderPath)) {
                foreach ((array) json_decode($mail->attachments) as $attachment) {
                    $filePath = Path::check($folderPath . '/' . $attachment->file);

                    if (is_file($filePath)) {
                        $this->mailer->addAttachment($filePath, $this->getAttachmentName($filePath, $attachment->name));
                    }
                }
            }
        }

        foreach ($this->attachments as $attachment) {
            if (is_file($attachment->file)) {
                $this->mailer->addAttachment($attachment->file, $this->getAttachmentName($attachment->file, $attachment->name));
            } else {
                $this->mailer->addStringAttachment($attachment->file, $attachment->name);
            }
        }

        return $this->mailer->Send();
    }

    /**
     * Replace tags with their values recursively
     *
     * @param   string  $text    The template to process
     * @param   array   $tags    An associative array to replace in the template
     * @param   bool    $isHtml  Is the text an HTML text and requires escaping
     *
     * @return  string  Rendered mail template
     *
     * @since   4.0.0
     */
    protected function replaceTags($text, $tags, $isHtml = false)
    {
        foreach ($tags as $key => $value) {
            // If the value is NULL, replace with an empty string. NULL itself throws notices
            if (\is_null($value)) {
                $value = '';
            }

            if (\is_array($value)) {
                $matches = [];
                $pregKey = preg_quote(strtoupper($key), '/');

                if (preg_match_all('/{' . $pregKey . '}(.*?){\/' . $pregKey . '}/s', $text, $matches)) {
                    foreach ($matches[0] as $i => $match) {
                        $replacement = '';

                        foreach ($value as $name => $subvalue) {
                            if (\is_array($subvalue) && $name == $matches[1][$i]) {
                                $subvalue = implode("\n", $subvalue);

                                // Escape if necessary
                                if ($isHtml && \in_array(strtoupper($key), $this->unsafe_tags, true)) {
                                    $subvalue = htmlspecialchars($subvalue, ENT_QUOTES, 'UTF-8');
                                }

                                $replacement .= implode("\n", $subvalue);
                            } elseif (\is_array($subvalue)) {
                                $replacement .= $this->replaceTags($matches[1][$i], $subvalue, $isHtml);
                            } elseif (\is_string($subvalue) && $name == $matches[1][$i]) {
                                // Escape if necessary
                                if ($isHtml && \in_array(strtoupper($key), $this->unsafe_tags, true)) {
                                    $subvalue = htmlspecialchars($subvalue, ENT_QUOTES, 'UTF-8');
                                }

                                $replacement .= $subvalue;
                            }
                        }

                        $text = str_replace($match, $replacement, $text);
                    }
                }
            } else {
                // Escape if necessary
                if ($isHtml && \in_array(strtoupper($key), $this->unsafe_tags, true)) {
                    $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
                }

                $text = str_replace('{' . strtoupper($key) . '}', $value, $text);
            }
        }

        return $text;
    }

    /**
     * Get a specific mail template
     *
     * @param   string  $key       Template identifier
     * @param   string  $language  Language code of the template
     *
     * @return  object|null  An object with the data of the mail, or null if the template not found in the db.
     *
     * @since   4.0.0
     */
    public static function getTemplate($key, $language)
    {
        $db    = Factory::getDbo();
        $query = $db->getQuery(true);
        $query->select('*')
            ->from($db->quoteName('#__mail_templates'))
            ->where($db->quoteName('template_id') . ' = :key')
            ->whereIn($db->quoteName('language'), ['', $language], ParameterType::STRING)
            ->order($db->quoteName('language') . ' DESC')
            ->bind(':key', $key);
        $db->setQuery($query);
        $mail = $db->loadObject();

        if ($mail) {
            $mail->params = new Registry($mail->params);
        }

        return $mail;
    }

    /**
     * Insert a new mail template into the system
     *
     * @param   string  $key       Mail template key
     * @param   string  $subject   A default subject (normally a translatable string)
     * @param   string  $body      A default body (normally a translatable string)
     * @param   array   $tags      Associative array of tags to replace
     * @param   string  $htmlbody  A default htmlbody (normally a translatable string)
     *
     * @return  boolean  True on success, false on failure
     *
     * @since   4.0.0
     */
    public static function createTemplate($key, $subject, $body, $tags, $htmlbody = '')
    {
        $db = Factory::getDbo();

        $template              = new \stdClass();
        $template->template_id = $key;
        $template->language    = '';
        $template->subject     = $subject;
        $template->body        = $body;
        $template->htmlbody    = $htmlbody;
        $template->extension   = explode('.', $key, 2)[0] ?? '';
        $template->attachments = '';
        $params                = new \stdClass();
        $params->tags          = (array) $tags;
        $template->params      = json_encode($params);

        return $db->insertObject('#__mail_templates', $template);
    }

    /**
     * Update an existing mail template
     *
     * @param   string  $key       Mail template key
     * @param   string  $subject   A default subject (normally a translatable string)
     * @param   string  $body      A default body (normally a translatable string)
     * @param   array   $tags      Associative array of tags to replace
     * @param   string  $htmlbody  A default htmlbody (normally a translatable string)
     *
     * @return  boolean  True on success, false on failure
     *
     * @since   4.0.0
     */
    public static function updateTemplate($key, $subject, $body, $tags, $htmlbody = '')
    {
        $db = Factory::getDbo();

        $template              = new \stdClass();
        $template->template_id = $key;
        $template->language    = '';
        $template->subject     = $subject;
        $template->body        = $body;
        $template->htmlbody    = $htmlbody;
        $params                = new \stdClass();
        $params->tags          = (array) $tags;
        $template->params      = json_encode($params);

        return $db->updateObject('#__mail_templates', $template, ['template_id', 'language']);
    }

    /**
     * Method to delete a mail template
     *
     * @param   string  $key  The key of the mail template
     *
     * @return  boolean  True on success, false on failure
     *
     * @since   4.0.0
     */
    public static function deleteTemplate($key)
    {
        $db    = Factory::getDbo();
        $query = $db->getQuery(true);
        $query->delete($db->quoteName('#__mail_templates'))
            ->where($db->quoteName('template_id') . ' = :key')
            ->bind(':key', $key);
        $db->setQuery($query);

        return $db->execute();
    }

    /**
     * Check and if necessary fix the file name of an attachment so that the attached file
     * has the same extension as the source file, and not a different file extension
     *
     * @param   string  $file  Path to the file to be attached
     * @param   string  $name  The file name to be used for the attachment
     *
     * @return  string  The corrected file name for the attachment
     *
     * @since   4.0.0
     */
    protected function getAttachmentName(string $file, string $name): string
    {
        // If no name is given, do not process it further
        if (!trim($name)) {
            return '';
        }

        // Replace any placeholders.
        $name = $this->replaceTags($name, $this->data);

        // Get the file extension.
        $ext = File::getExt($file);

        // Strip off extension from $name and append extension of $file, if any
        return File::stripExt($name) . ($ext ? '.' . $ext : '');
    }
}
