<?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;

\defined('JPATH_PLATFORM') or die;

use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Mail\Exception\MailDisabledException;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use PHPMailer\PHPMailer\Exception as phpmailerException;

/**
 * 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 = array();

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

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

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

	/**
	 * 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 replace in the template
	 *
	 * @param   array  $data  Associative array of strings to replace
	 *
	 * @return  void
	 *
	 * @since   4.0.0
	 */
	public function addTemplateData($data)
	{
		$this->data = array_merge($this->data, $data);
	}

	/**
	 * 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();

		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);
			}
		}

		$app->triggerEvent('onMailBeforeRendering', array($this->template_id, &$this));

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

		$mailStyle = $config->get('mail_style', 'plaintext');
		$plainBody = $this->replaceTags(Text::_($mail->body), $this->data);
		$htmlBody  = $this->replaceTags(Text::_($mail->htmlbody), $this->data);

		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($plainBody, false);
			}

			$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);
		}

		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
	 *
	 * @return  string  Rendered mail template
	 *
	 * @since   4.0.0
	 */
	protected function replaceTags($text, $tags)
	{
		foreach ($tags as $key => $value)
		{
			if (is_array($value))
			{
				$matches = array();

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

						foreach ($value as $subvalue)
						{
							if (is_array($subvalue))
							{
								$replacement .= $this->replaceTags($matches[1][$i], $subvalue);
							}
						}

						$text = str_replace($match, $replacement, $text);
					}
				}
			}
			else
			{
				$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->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 : '');
	}
}
