diff --git a/_extensions/s2_blog/Extension.php b/_extensions/s2_blog/Extension.php index be4f5b35..910562d9 100644 --- a/_extensions/s2_blog/Extension.php +++ b/_extensions/s2_blog/Extension.php @@ -15,6 +15,7 @@ use S2\Cms\Controller\CommentController; use S2\Cms\Framework\Container; use S2\Cms\Framework\ExtensionInterface; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Model\Article\ArticleRenderedEvent; use S2\Cms\Model\ArticleProvider; use S2\Cms\Model\AuthProvider; @@ -280,6 +281,7 @@ public function buildContainer(Container $container): void $container->get(HtmlTemplateProvider::class), $container->get(Viewer::class), $container->get(LoggerInterface::class), + $container->get(CommentMailer::class), $provider->get('S2_ENABLED_COMMENTS') === '1', $provider->get('S2_PREMODERATION') === '1', ); @@ -297,6 +299,7 @@ public function buildContainer(Container $container): void $container->get(DbLayer::class), $container->get(UrlBuilder::class), $container->get(BlogUrlBuilder::class), + $container->get(CommentMailer::class), ); }); diff --git a/_extensions/s2_blog/Model/BlogCommentNotifier.php b/_extensions/s2_blog/Model/BlogCommentNotifier.php index e9193b8f..a6a6f171 100644 --- a/_extensions/s2_blog/Model/BlogCommentNotifier.php +++ b/_extensions/s2_blog/Model/BlogCommentNotifier.php @@ -9,6 +9,7 @@ namespace s2_extensions\s2_blog\Model; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Model\UrlBuilder; use S2\Cms\Pdo\DbLayer; use S2\Cms\Pdo\DbLayerException; @@ -29,6 +30,7 @@ public function __construct( private DbLayer $dbLayer, private UrlBuilder $urlBuilder, private BlogUrlBuilder $blogUrlBuilder, + private CommentMailer $commentMailer, ) { } @@ -64,10 +66,6 @@ public function notify(int $commentId): void * We have to send the comment to subscribed commentators. */ - if (!defined('S2_COMMENTS_FUNCTIONS_LOADED')) { - require S2_ROOT . '_include/comments.php'; - } - // Getting some info about the post commented $result = $this->dbLayer->buildAndQuery([ 'SELECT' => 'title, create_time, url', @@ -102,7 +100,7 @@ public function notify(int $commentId): void 'code=' . $receiver['hash'], ]); - s2_mail_comment($receiver['nick'], $receiver['email'], $message, $post['title'], $link, $comment['nick'], $unsubscribeLink); + $this->commentMailer->mailToSubscriber($receiver['nick'], $receiver['email'], $message, $post['title'], $link, $comment['nick'], $unsubscribeLink); } // Toggle sent mark diff --git a/_extensions/s2_blog/hooks/cmnt_pre_get_comment_count_qr.php b/_extensions/s2_blog/hooks/cmnt_pre_get_comment_count_qr.php deleted file mode 100644 index fb5a4fbf..00000000 --- a/_extensions/s2_blog/hooks/cmnt_pre_get_comment_count_qr.php +++ /dev/null @@ -1,19 +0,0 @@ - 'count(id)', - 'FROM' => 's2_blog_comments', - 'WHERE' => 'post_id = '.$id.' AND shown = 1' - ); diff --git a/_extensions/s2_blog/hooks/cmnt_pre_get_page_info_qr.php b/_extensions/s2_blog/hooks/cmnt_pre_get_page_info_qr.php deleted file mode 100644 index 80fe9d34..00000000 --- a/_extensions/s2_blog/hooks/cmnt_pre_get_page_info_qr.php +++ /dev/null @@ -1,19 +0,0 @@ - 'create_time, url, title, 0 AS parent_id', - 'FROM' => 's2_blog_posts', - 'WHERE' => 'id = '.$id.' AND published = 1 AND commented = 1' - ); diff --git a/_extensions/s2_blog/hooks/cmnt_pre_get_subscribers_qr.php b/_extensions/s2_blog/hooks/cmnt_pre_get_subscribers_qr.php deleted file mode 100644 index cdf3cfcc..00000000 --- a/_extensions/s2_blog/hooks/cmnt_pre_get_subscribers_qr.php +++ /dev/null @@ -1,21 +0,0 @@ - 'id, nick, email, ip, time', - 'FROM' => 's2_blog_comments', - 'WHERE' => 'post_id = '.$id.' AND subscribed = 1 AND shown = 1 AND email <> \''.$s2_db->escape($email).'\'' - ); diff --git a/_extensions/s2_blog/hooks/cmnt_pre_path_check.php b/_extensions/s2_blog/hooks/cmnt_pre_path_check.php deleted file mode 100644 index 5f3f84ab..00000000 --- a/_extensions/s2_blog/hooks/cmnt_pre_path_check.php +++ /dev/null @@ -1,15 +0,0 @@ - 'id, nick, email, ip, time', - 'FROM' => 's2_blog_comments', - 'WHERE' => 'post_id = '.$id.' and subscribed = 1 and email = \''.$s2_db->escape($_GET['mail']).'\'' - ); diff --git a/_extensions/s2_blog/hooks/cmnt_unsubscribe_pre_upd_qr.php b/_extensions/s2_blog/hooks/cmnt_unsubscribe_pre_upd_qr.php deleted file mode 100644 index 04fe1a44..00000000 --- a/_extensions/s2_blog/hooks/cmnt_unsubscribe_pre_upd_qr.php +++ /dev/null @@ -1,21 +0,0 @@ - 's2_blog_comments', - 'SET' => 'subscribed = 0', - 'WHERE' => 'post_id = '.$id.' and subscribed = 1 and email = \''.$s2_db->escape($_GET['mail']).'\'' - ); diff --git a/_include/comments.php b/_include/comments.php deleted file mode 100644 index 4fcef991..00000000 --- a/_include/comments.php +++ /dev/null @@ -1,249 +0,0 @@ -', '', '', '<url>', '<text>', '<unsubscribe>'), - array($name, $auth_name, $title, $url, $text, $unsubscribe_link), $message); - - // Make sure all linebreaks are CRLF in message (and strip out any NULL bytes) - $message = str_replace(array("\n", "\0"), array("\r\n", ''), $message); - - $subject = sprintf(Lang::get('Email subject', 'comments'), $url); - $subject = "=?UTF-8?B?".base64_encode($subject)."?="; - - $sender_email = S2_WEBMASTER_EMAIL ? S2_WEBMASTER_EMAIL : 'example@example.com'; - $from = S2_WEBMASTER ? "=?UTF-8?B?".base64_encode(S2_WEBMASTER)."?=".' <'.$sender_email.'>' : $sender_email; - $headers = 'From: '.$from."\r\n". - 'Date: '.gmdate('r')."\r\n". - 'MIME-Version: 1.0'."\r\n". - 'Content-transfer-encoding: 8bit'."\r\n". - 'Content-type: text/plain; charset=utf-8'."\r\n". - 'X-Mailer: S2 Mailer'."\r\n". - 'List-Unsubscribe: <'.$unsubscribe_link.'>'."\r\n". - 'Reply-To: '.$from; - - if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80000) { - // Change the linebreaks used in the headers according to OS - if (strtoupper(substr(PHP_OS, 0, 3)) === 'MAC') { - $headers = str_replace("\r\n", "\r", $headers); - } - else if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - $headers = str_replace("\r\n", "\n", $headers); - } - } - - mail($email, $subject, $message, $headers); -} - -// -// Sends comments to subscribed users -// -function s2_mail_moderator ($name, $email, $text, $title, $url, $authorName, $authorEmail) -{ - $message = Lang::get('Email moderator pattern', 'comments'); - $message = str_replace(array('<name>', '<author>', '<title>', '<url>', '<text>'), - array($name, $authorName, $title, $url, $text), $message); - - // Make sure all linebreaks are CRLF in message (and strip out any NULL bytes) - $message = str_replace(array("\n", "\0"), array("\r\n", ''), $message); - - $subject = sprintf(Lang::get('Email subject', 'comments'), $url); - $subject = "=?UTF-8?B?".base64_encode($subject)."?="; - - // Our email - $sender_email = S2_WEBMASTER_EMAIL ? S2_WEBMASTER_EMAIL : 'example@example.com'; - $sender = S2_WEBMASTER ? "=?UTF-8?B?".base64_encode(S2_WEBMASTER)."?=".' <'.$sender_email.'>' : $sender_email; - - // Author email - $from = trim($authorName) ? "=?UTF-8?B?".base64_encode($authorName)."?=".' <'.$authorEmail.'>' : $authorEmail; - $headers = - 'From: '.$sender."\r\n". // One cannot use the real author email in "From:" header due to DMARC. Use our one. - 'Sender: '.$from."\r\n". // Let's use the real author email at least here. - 'Date: '.gmdate('r')."\r\n". - 'MIME-Version: 1.0'."\r\n". - 'Content-transfer-encoding: 8bit'."\r\n". - 'Content-type: text/plain; charset=utf-8'."\r\n". - 'X-Mailer: S2 Mailer'."\r\n". - 'Reply-To: '.$from; - - if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80000) { - // Change the linebreaks used in the headers according to OS - if (strtoupper(substr(PHP_OS, 0, 3)) === 'MAC') { - $headers = str_replace("\r\n", "\r", $headers); - } - else if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - $headers = str_replace("\r\n", "\n", $headers); - } - } - - mail($email, $subject, $message, $headers); -} - -function s2_link_count(string $text) -{ - return preg_match_all('#(https?://\S{2,}?)(?=[\s),\'><\]]|<|>|[.;:](?:\s|$)|$)#u', $text); -} - -// -// Parses BB-codes in comments -// -function s2_bbcode_to_html ($s) -{ - $s = str_replace("''", '"', $s); - $s = str_replace("\r", '', $s); - - $s = preg_replace('#\[I\](.*?)\[/I\]#isS', '<em>\1</em>', $s); - $s = preg_replace('#\[B\](.*?)\[/B\]#isS', '<strong>\1</strong>', $s); - - while (preg_match ('/\[Q\s*=\s*([^\]]*)\].*?\[\/Q\]/isS', $s)) - $s = preg_replace('/\s*\[Q\s*=\s*([^\]]*)\]\s*(.*?)\s*\[\/Q\]\s*/isS', '<blockquote><strong>\\1</strong> '.Lang::get('Wrote').'<br/><br/><em>\\2</em></blockquote>', $s); - - while (preg_match ('/\[Q\].*?\[\/Q\]/isS', $s)) - $s = preg_replace('/\s*\[Q\]\s*(.*?)\s*\[\/Q\]\s*/isS', '<blockquote>\\1</blockquote>', $s); - - $s = preg_replace_callback( - '#(https?://\S{2,}?)(?=[\s),\'><\]]|<|>|[.;:](?:\s|$)|$)#u', - function ($matches) - { - $href = $link = $matches[1]; - - if (mb_strlen($matches[1]) > 55) - $link = mb_substr($matches[1], 0 , 42).' … '.mb_substr($matches[1], -10); - - return '<noindex><a href="'.$href.'" rel="nofollow">'.$link.'</a></noindex>'; - }, - $s - ); - $s = str_replace("\n", '<br />', $s); - return $s; -} - -// -// wordwrap() with utf-8 support -// -function utf8_wordwrap($string, $width = 75, $break = "\n") -{ - $a = explode("\n", $string); - foreach ($a as $k => $str) - { - $str = preg_split('#[\s\r]+#', $str); - $len = 0; - $return = ''; - foreach ($str as $val) - { - $val .= ' '; - $tmp = mb_strlen($val); - $len += $tmp; - if ($len >= $width) - { - $return .= $break . $val; - $len = $tmp; - } - else - $return .= $val; - } - $a[$k] = $return; - } - return implode("\n", $a); -} - -// -// Parses BB-codes in comments and makes quotes mail-styled (used '>') -// -function s2_bbcode_to_mail ($s) -{ - $s = str_replace("\r", '', $s); - $s = str_replace(array('"', '«', '»'), '"', $s); - $s = preg_replace('/\[I\s*?\](.*?)\[\/I\s*?\]/isu', "_\\1_", $s); - $s = preg_replace('/\[B\s*?\](.*?)\[\/B\s*?\]/isu', "*\\1*", $s); - - // Do not ask me how the rest of the function works. - // It just works :) - - while (preg_match ('/\[Q\s*?=?\s*?([^\]]*)\s*?\].*?\[\/Q.*?\]/is', $s)) - $s = preg_replace('/\s*\[Q\s*?=?\s*?([^\]]*)\s*?\]\s*(.*?)\s*\[\/Q.*?\]\s*/is', "<q/>\\2</q>", $s); - - $strings = $levels = array(); - - $curr = 0; - $level = 0; - - while (1) - { - $up = strpos($s, '<q/>', $curr); - $down = strpos($s, '</q>', $curr); - if ($up === false) - { - if ($down ===false) - break; - $dl = -1; - $c = $down; - } - elseif ($down === false || $up < $down) - { - $dl = 1; - $c = $up; - } - else - { - $dl = -1; - $c = $down; - } - $strings[] = substr($s, $curr, $c - $curr); - $curr = $c + 4; - $levels[] = $level; - $level += $dl; - } - - $strings[] = substr($s, $curr); - $levels[] = 0; - - $out = array(); - foreach ($strings as $i => $string) - { - if (trim($string) == '') - continue; - $delimeter = "\n".str_repeat('> ', $levels[$i]); - $out[] = $delimeter.utf8_wordwrap(str_replace("\n", $delimeter, $string), 70 - 2*$levels[$i], $delimeter); - } - - $s = implode ("\n", $out); - - return trim($s); -} - -define('S2_COMMENTS_FUNCTIONS_LOADED', 1); diff --git a/_include/functions.php b/_include/functions.php index b8fac0c3..b2a68439 100644 --- a/_include/functions.php +++ b/_include/functions.php @@ -488,3 +488,120 @@ function s2_get_config_filename(): string return 'config.php'; } + + +// +// Parses BB-codes in comments +// +function s2_bbcode_to_html($s) +{ + $s = str_replace("''", '"', $s); + $s = str_replace("\r", '', $s); + + $s = preg_replace('#\[I\](.*?)\[/I\]#isS', '<em>\1</em>', $s); + $s = preg_replace('#\[B\](.*?)\[/B\]#isS', '<strong>\1</strong>', $s); + + while (preg_match('/\[Q\s*=\s*([^\]]*)\].*?\[\/Q\]/isS', $s)) + $s = preg_replace('/\s*\[Q\s*=\s*([^\]]*)\]\s*(.*?)\s*\[\/Q\]\s*/isS', '<blockquote><strong>\\1</strong> ' . Lang::get('Wrote') . '<br/><br/><em>\\2</em></blockquote>', $s); + + while (preg_match('/\[Q\].*?\[\/Q\]/isS', $s)) + $s = preg_replace('/\s*\[Q\]\s*(.*?)\s*\[\/Q\]\s*/isS', '<blockquote>\\1</blockquote>', $s); + + $s = preg_replace_callback( + '#(https?://\S{2,}?)(?=[\s),\'><\]]|<|>|[.;:](?:\s|$)|$)#u', + function ($matches) { + $href = $link = $matches[1]; + + if (mb_strlen($matches[1]) > 55) + $link = mb_substr($matches[1], 0, 42) . ' … ' . mb_substr($matches[1], -10); + + return '<noindex><a href="' . $href . '" rel="nofollow">' . $link . '</a></noindex>'; + }, + $s + ); + $s = str_replace("\n", '<br />', $s); + return $s; +} + +// +// wordwrap() with utf-8 support +// +function s2_utf8_wordwrap($string, $width = 75, $break = "\n") +{ + $a = explode("\n", $string); + foreach ($a as $k => $str) { + $str = preg_split('#[\s\r]+#', $str); + $len = 0; + $return = ''; + foreach ($str as $val) { + $val .= ' '; + $tmp = mb_strlen($val); + $len += $tmp; + if ($len >= $width) { + $return .= $break . $val; + $len = $tmp; + } else + $return .= $val; + } + $a[$k] = $return; + } + return implode("\n", $a); +} + +// +// Parses BB-codes in comments and makes quotes mail-styled (used '>') +// +function s2_bbcode_to_mail($s) +{ + $s = str_replace("\r", '', $s); + $s = str_replace(array('"', '«', '»'), '"', $s); + $s = preg_replace('/\[I\s*?\](.*?)\[\/I\s*?\]/isu', "_\\1_", $s); + $s = preg_replace('/\[B\s*?\](.*?)\[\/B\s*?\]/isu', "*\\1*", $s); + + // Do not ask me how the rest of the function works. + // It just works :) + + while (preg_match('/\[Q\s*?=?\s*?([^\]]*)\s*?\].*?\[\/Q.*?\]/is', $s)) + $s = preg_replace('/\s*\[Q\s*?=?\s*?([^\]]*)\s*?\]\s*(.*?)\s*\[\/Q.*?\]\s*/is', "<q/>\\2</q>", $s); + + $strings = $levels = array(); + + $curr = 0; + $level = 0; + + while (1) { + $up = strpos($s, '<q/>', $curr); + $down = strpos($s, '</q>', $curr); + if ($up === false) { + if ($down === false) + break; + $dl = -1; + $c = $down; + } elseif ($down === false || $up < $down) { + $dl = 1; + $c = $up; + } else { + $dl = -1; + $c = $down; + } + $strings[] = substr($s, $curr, $c - $curr); + $curr = $c + 4; + $levels[] = $level; + $level += $dl; + } + + $strings[] = substr($s, $curr); + $levels[] = 0; + + $out = array(); + foreach ($strings as $i => $string) { + if (trim($string) == '') + continue; + $delimiter = "\n" . str_repeat('> ', $levels[$i]); + $out[] = $delimiter . s2_utf8_wordwrap(str_replace("\n", $delimiter, $string), 70 - 2 * $levels[$i], $delimiter); + } + + $s = implode("\n", $out); + + return trim($s); +} diff --git a/_include/src/CmsExtension.php b/_include/src/CmsExtension.php index 4d806e82..e88f91a2 100644 --- a/_include/src/CmsExtension.php +++ b/_include/src/CmsExtension.php @@ -30,6 +30,7 @@ use S2\Cms\Image\ThumbnailGenerator; use S2\Cms\Layout\LayoutMatcherFactory; use S2\Cms\Logger\Logger; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Model\ArticleProvider; use S2\Cms\Model\AuthProvider; use S2\Cms\Model\Comment\ArticleCommentStrategy; @@ -354,11 +355,19 @@ public function buildContainer(Container $container): void ); }); + $container->set(CommentMailer::class, function (Container $container) { + return new CommentMailer( + $container->get('comments_translator'), + $container->get(DynamicConfigProvider::class) + ); + }); + $container->set(CommentNotifier::class, function (Container $container) { return new CommentNotifier( $container->get(DbLayer::class), $container->get(ArticleProvider::class), $container->get(UrlBuilder::class), + $container->get(CommentMailer::class), ); }); @@ -405,6 +414,7 @@ public function buildContainer(Container $container): void $container->get(HtmlTemplateProvider::class), $container->get(Viewer::class), $container->get(LoggerInterface::class), + $container->get(CommentMailer::class), $provider->get('S2_ENABLED_COMMENTS') === '1', $provider->get('S2_PREMODERATION') === '1', ); @@ -417,6 +427,7 @@ public function buildContainer(Container $container): void $container->get('comments_translator'), $container->get(UrlBuilder::class), $container->get(HtmlTemplateProvider::class), + $container->get(CommentMailer::class), ...$container->getByTag(CommentStrategyInterface::class) ); }); diff --git a/_include/src/Controller/CommentController.php b/_include/src/Controller/CommentController.php index 7418fa61..5b20c54d 100644 --- a/_include/src/Controller/CommentController.php +++ b/_include/src/Controller/CommentController.php @@ -11,6 +11,7 @@ use Psr\Log\LoggerInterface; use S2\Cms\Framework\ControllerInterface; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Model\AuthProvider; use S2\Cms\Model\Comment\CommentStrategyInterface; use S2\Cms\Model\UrlBuilder; @@ -35,6 +36,7 @@ public function __construct( private HtmlTemplateProvider $templateProvider, private Viewer $viewer, private LoggerInterface $logger, + private CommentMailer $commentMailer, private bool $commentsEnabled, private bool $premoderationEnabled, ) { @@ -172,9 +174,8 @@ public function handle(Request $request): Response $message = s2_bbcode_to_mail($text); // Sending the comment to moderators - foreach ($this->userProvider->getModerators([], $moderationRequired && $isOnline ? [$email] : []) as $moderator) { - s2_mail_moderator($moderator->login, $moderator->email, $message, $target->title, $link, $name, $email); + $this->commentMailer->mailToModerator($moderator->login, $moderator->email, $message, $target->title, $link, $name, $email); } if (!$moderationRequired) { diff --git a/_include/src/Controller/CommentSentController.php b/_include/src/Controller/CommentSentController.php index 9d310600..0e1053e5 100644 --- a/_include/src/Controller/CommentSentController.php +++ b/_include/src/Controller/CommentSentController.php @@ -10,6 +10,7 @@ namespace S2\Cms\Controller; use S2\Cms\Framework\ControllerInterface; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Model\AuthProvider; use S2\Cms\Model\Comment\CommentStrategyInterface; use S2\Cms\Model\UrlBuilder; @@ -37,6 +38,7 @@ public function __construct( private TranslatorInterface $translator, private UrlBuilder $urlBuilder, private HtmlTemplateProvider $templateProvider, + private CommentMailer $commentMailer, CommentStrategyInterface ...$strategies ) { $this->commentStrategies = $strategies; @@ -72,7 +74,7 @@ public function handle(Request $request): Response $message = s2_bbcode_to_mail($comment->text); $target = $commentStrategy->getTargetById($comment->targetId); foreach ($moderators as $moderator) { - s2_mail_moderator($moderator->login, $moderator->email, $message, $target->title ?? 'unknown item', $link, $comment->name, $comment->email); + $this->commentMailer->mailToModerator($moderator->login, $moderator->email, $message, $target->title ?? 'unknown item', $link, $comment->name, $comment->email); } } break; diff --git a/_include/src/Mail/CommentMailer.php b/_include/src/Mail/CommentMailer.php new file mode 100644 index 00000000..a7cbd881 --- /dev/null +++ b/_include/src/Mail/CommentMailer.php @@ -0,0 +1,142 @@ +<?php +/** + * @copyright 2009-2024 Roman Parpalak + * @license https://opensource.org/license/mit MIT + * @package S2 + */ + +declare(strict_types=1); + +namespace S2\Cms\Mail; + +use S2\Cms\Config\DynamicConfigProvider; +use Symfony\Contracts\Translation\TranslatorInterface; + +readonly class CommentMailer +{ + public function __construct( + private TranslatorInterface $translator, + private DynamicConfigProvider $dynamicConfigProvider + ) { + } + + public function mailToSubscriber( + string $subscriberName, + string $subscriberEmail, + string $text, + string $title, + string $url, + string $authorName, + string $unsubscribeLink + ): bool { + $messageTemplate = $this->translator->trans('Email pattern'); + $message = str_replace( + ['<name>', '<author>', '<title>', '<url>', '<text>', '<unsubscribe>'], + [$subscriberName, $authorName, $title, $url, $text, $unsubscribeLink], + $messageTemplate + ); + + // Make sure all linebreaks are CRLF in message (and strip out any NULL bytes) + $message = str_replace(["\n", "\0"], ["\r\n", ''], $message); + + $subject = sprintf($this->translator->trans('Email subject'), $url); + $subject = "=?UTF-8?B?" . base64_encode($subject) . "?="; + + // Our email + $from = $this->getWebmasterNameAndEmail(); + + $headers = [ + 'From' => $from, + 'Date' => gmdate('r'), + 'MIME-Version' => '1.0', + 'Content-transfer-encoding' => '8bit', + 'Content-type' => 'text/plain; charset=utf-8', + 'X-Mailer' => 'S2 Mailer', + 'List-Unsubscribe' => '<' . $unsubscribeLink . '>', + 'Reply-To' => $from + ]; + + return $this->sendMail($subscriberEmail, $subject, $message, $headers); + } + + public function mailToModerator( + string $moderatorName, + string $moderatorEmail, + string $text, + string $title, + string $url, + string $authorName, + string $authorEmail + ): bool { + $messageTemplate = $this->translator->trans('Email moderator pattern'); + $message = str_replace( + ['<name>', '<author>', '<title>', '<url>', '<text>'], + [$moderatorName, $authorName, $title, $url, $text], + $messageTemplate + ); + + // Make sure all linebreaks are CRLF in message (and strip out any NULL bytes) + $message = str_replace(["\n", "\0"], ["\r\n", ''], $message); + + $subject = sprintf($this->translator->trans('Email subject'), $url); + $subject = "=?UTF-8?B?" . base64_encode($subject) . "?="; + + // Our email + $webmaster = $this->getWebmasterNameAndEmail(); + + // Author email + $author = "=?UTF-8?B?" . base64_encode($authorName) . "?=" . ' <' . $authorEmail . '>'; + + $headers = [ + 'From' => $webmaster, // One cannot use the real author email in "From:" header due to DMARC. Use our one. + 'Sender' => $author, // Let's use the real author email at least here. + 'Date' => gmdate('r'), + 'MIME-Version' => '1.0', + 'Content-transfer-encoding' => '8bit', + 'Content-type' => 'text/plain; charset=utf-8', + 'X-Mailer' => 'S2 Mailer', + 'Reply-To' => $author + ]; + + return $this->sendMail($moderatorEmail, $subject, $message, $headers); + } + + private function sendMail(string $email, string $subject, string $message, array $headers): bool + { + $headersFormatted = $this->formatHeaders($headers); + + if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80000) { + // Old hack for PHP < 8.0 + // Change the linebreaks used in the headers according to OS + $os = strtoupper(substr(PHP_OS, 0, 3)); + if ($os === 'MAC') { + $headersFormatted = str_replace("\r\n", "\r", $headersFormatted); + } elseif ($os !== 'WIN') { + $headersFormatted = str_replace("\r\n", "\n", $headersFormatted); + } + } + + return mail($email, $subject, $message, $headersFormatted); + } + + private function formatHeaders(array $headers): string + { + $formatted = ''; + foreach ($headers as $key => $value) { + $formatted .= $key . ': ' . $value . "\r\n"; + } + return $formatted; + } + + private function getWebmasterNameAndEmail(): string + { + $email = $this->dynamicConfigProvider->get('S2_WEBMASTER_EMAIL') ?: 'example@example.com'; + $name = $this->dynamicConfigProvider->get('S2_WEBMASTER'); + + if ($name) { + return "=?UTF-8?B?" . base64_encode($name) . "?=" . ' <' . $email . '>'; + } + + return $email; + } +} diff --git a/_include/src/Model/CommentNotifier.php b/_include/src/Model/CommentNotifier.php index 30f2f7c8..b916cecd 100644 --- a/_include/src/Model/CommentNotifier.php +++ b/_include/src/Model/CommentNotifier.php @@ -9,6 +9,7 @@ namespace S2\Cms\Model; +use S2\Cms\Mail\CommentMailer; use S2\Cms\Pdo\DbLayer; use S2\Cms\Pdo\DbLayerException; @@ -27,6 +28,7 @@ public function __construct( private DbLayer $dbLayer, private ArticleProvider $articleProvider, private UrlBuilder $urlBuilder, + private CommentMailer $commentMailer, ) { } @@ -62,10 +64,6 @@ public function notify(int $commentId): void * We have to send the comment to subscribed commentators. */ - if (!defined('S2_COMMENTS_FUNCTIONS_LOADED')) { - require S2_ROOT . '_include/comments.php'; - } - // Getting some info about the article commented $result = $this->dbLayer->buildAndQuery([ 'SELECT' => 'title, parent_id, url', @@ -106,7 +104,7 @@ public function notify(int $commentId): void 'code=' . $receiver['hash'], ]); - s2_mail_comment($receiver['nick'], $receiver['email'], $message, $article['title'], $link, $comment['nick'], $unsubscribeLink); + $this->commentMailer->mailToSubscriber($receiver['nick'], $receiver['email'], $message, $article['title'], $link, $comment['nick'], $unsubscribeLink); } // Toggle sent mark diff --git a/_include/src/Model/Model.php b/_include/src/Model/Model.php deleted file mode 100644 index 02587ef5..00000000 --- a/_include/src/Model/Model.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace S2\Cms\Model; - -use S2\Cms\Pdo\DbLayer; - -/** - * Helper functions for handling pages stored in DB. - * - * @copyright (C) 2007-2014 Roman Parpalak - * @license http://www.gnu.org/licenses/gpl.html GPL version 2 or higher - * @package S2 - * - * @deprecated Do not use static calls like this in a new code. - */ -class Model -{ - const ROOT_ID = 0; - - - /** - * Returns the full path for an article - * - * @deprecated Do not use static calls like this in a new code. - * @throws \S2\Cms\Pdo\DbLayerException - */ - public static function path_from_id($id, $visible_for_all = false) - { - /** @var DbLayer $s2_db */ - $s2_db = \Container::get(DbLayer::class); - - if ($id < 0) - return false; - - if ($id == self::ROOT_ID) - return ''; - - $query = array( - 'SELECT' => 'url, parent_id', - 'FROM' => 'articles', - 'WHERE' => 'id = ' . $id . ($visible_for_all ? ' AND published = 1' : '') - ); - ($hook = s2_hook('fn_path_from_id_pre_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - - $row = $s2_db->fetchRow($result); - if (!$row) - return false; - - if ($row[1] == self::ROOT_ID) - return ''; - - if (S2_USE_HIERARCHY) { - $prefix = self::path_from_id($row[1], $visible_for_all); - if ($prefix === false) - return false; - } else - $prefix = ''; - - return $prefix . '/' . rawurlencode($row[0]); - } -} diff --git a/comment.php b/comment.php deleted file mode 100644 index 4aee198e..00000000 --- a/comment.php +++ /dev/null @@ -1,343 +0,0 @@ -<?php -/** - * Receives POST data and saves user comments. - * - * @copyright (C) 2009-2024 Roman Parpalak - * @license http://www.gnu.org/licenses/gpl.html GPL version 2 or higher - * @package S2 - */ - -use Psr\Log\LoggerInterface; -use S2\Cms\Model\Model; -use S2\Cms\Pdo\DbLayer; -use S2\Cms\Template\HtmlTemplateProvider; - -define('S2_ROOT', './'); -require S2_ROOT.'_include/common.php'; -require S2_ROOT.'_include/comments.php'; - -($hook = s2_hook('cmnt_start')) ? eval($hook) : null; - -header('X-Powered-By: S2/'.S2_VERSION); - -/** @var HtmlTemplateProvider $templateProvider */ -$templateProvider = $app->container->get(HtmlTemplateProvider::class); -$template = $templateProvider->getTemplate('service.php'); - -if (isset($_GET['go'])) -{ - // Outputs "comment saved" message (used if the premoderation mode is enabled) - $template - ->putInPlaceholder('head_title', '✅' . Lang::get('Comment sent', 'comments')) - ->putInPlaceholder('title', '<span class="icon-success">✔</span>' . Lang::get('Comment sent', 'comments')) - ->putInPlaceholder('text', sprintf(Lang::get('Comment sent info', 'comments'), s2_htmlencode($_GET['go']), s2_link('/'))) - ; - - $template->toHttpResponse()->send(); - - die(); -} - -if (isset($_GET['unsubscribe'])) -{ - header('Content-Type: text/html; charset=utf-8'); - - if (isset($_GET['id'], $_GET['mail'])) { - /** @var DbLayer $s2_db */ - $s2_db = $app->container->get(DbLayer::class); - - [$id, $class] = explode('.', $_GET['id']); - $id = (int) $id; - $class = (string) $class; - - $query = [ - 'SELECT' => 'id, nick, email, ip, time', - 'FROM' => 'art_comments', - 'WHERE' => 'article_id = '.$id.' and subscribed = 1 and email = \''.$s2_db->escape($_GET['mail']).'\'' - ]; - ($hook = s2_hook('cmnt_unsubscribe_pre_get_receivers_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - - $found = false; - while ($receiver = $s2_db->fetchAssoc($result)) { - if ($_GET['unsubscribe'] === base_convert(substr(md5($receiver['id'] . $receiver['ip'] . $receiver['nick'] . $receiver['email'] . $receiver['time']), 0, 16), 16, 36)) { - $found = true; - } - } - - if ($found) { - $query = [ - 'UPDATE' => 'art_comments', - 'SET' => 'subscribed = 0', - 'WHERE' => 'article_id = '.$id.' and subscribed = 1 and email = \''.$s2_db->escape($_GET['mail']).'\'' - ]; - ($hook = s2_hook('cmnt_unsubscribe_pre_upd_qr')) ? eval($hook) : null; - $s2_db->buildAndQuery($query); - - $template - ->putInPlaceholder('head_title', Lang::get('Unsubscribed OK', 'comments')) - ->putInPlaceholder('title', Lang::get('Unsubscribed OK', 'comments')) - ->putInPlaceholder('text', Lang::get('Unsubscribed OK info', 'comments')) - ; - - $template->toHttpResponse()->send(); - - die(); - } - } - - $template - ->putInPlaceholder('head_title', Lang::get('Unsubscribed failed', 'comments')) - ->putInPlaceholder('title', Lang::get('Unsubscribed failed', 'comments')) - ->putInPlaceholder('text', Lang::get('Unsubscribed failed info', 'comments')) - ; - - $template->toHttpResponse()->send(); - - die(); -} - -if (!defined('S2_MAX_COMMENT_BYTES')) { - define('S2_MAX_COMMENT_BYTES', 65535); -} - -$_POST['show_email'] = $show_email = (int) isset($_POST['show_email']); -$_POST['subscribed'] = $subscribed = (int) isset($_POST['subscribed']); - -($hook = s2_hook('cmnt_pre_post_check')) ? eval($hook) : null; - -// -// Starting input validation -// - -$errors = []; - -if (!S2_ENABLED_COMMENTS) { - $errors[] = Lang::get('disabled', 'comments'); -} - -function s2_ext_var($field) -{ - return !empty($_POST[$field]) ? trim((string) $_POST[$field]) : ''; -} - -$text = s2_ext_var('text'); -if ($text === '') { - $errors[] = Lang::get('missing_text', 'comments'); -} -if (strlen($text) > S2_MAX_COMMENT_BYTES) { - $errors[] = sprintf(Lang::get('long_text', 'comments'), S2_MAX_COMMENT_BYTES); -} elseif (s2_link_count($text) > 0) { - $errors[] = Lang::get('links_in_text', 'comments'); -} - -$email = s2_ext_var('email'); -if (!s2_is_valid_email($email)) - $errors[] = Lang::get('email', 'comments'); - -$name = s2_ext_var('name'); -if (empty($name)) - $errors[] = Lang::get('missing_nick', 'comments'); -if (mb_strlen($name) > 50) - $errors[] = Lang::get('long_nick', 'comments'); - -if (count($errors) === 0 && !s2_check_comment_question($_POST['key'] ?? '', $_POST['question'] ?? '')) - $errors[] = Lang::get('question', 'comments'); - -$idArray = explode('.', s2_ext_var('id')); -if (count($idArray) !== 2) { - $errors[] = Lang::get('no_item', 'comments'); - $id = $class = null; -} else { - [$id, $class] = $idArray; - $id = (int) $id; -} - -($hook = s2_hook('cmnt_after_post_check')) ? eval($hook) : null; - -if (isset($_POST['preview'])) -{ - // Handling "Preview" button - - ($hook = s2_hook('cmnt_preview_pre_comment_merge')) ? eval($hook) : null; - - $viewer = $app->container->get(\S2\Cms\Template\Viewer::class); - $text_preview = '<p>' . Lang::get('Comment preview info', 'comments') . '</p>' . "\n" . - $viewer->render('comment', array( - 'text' => $text, - 'nick' => $name, - 'time' => time(), - 'email' => $email, - 'show_email' => $show_email, - )); - - $template - ->putInPlaceholder('head_title' , Lang::get('Comment preview', 'comments')) - ->putInPlaceholder('title' , Lang::get('Comment preview', 'comments')) - ->putInPlaceholder('text' , $text_preview) - ->putInPlaceholder('id' , $id) - ->putInPlaceholder('class' , $class) - ->putInPlaceholder('commented' , true) - ->putInPlaceholder('comment_form' , compact('name', 'email', 'show_email', 'subscribed', 'text')) - ; - - $template->toHttpResponse()->send(); - - die(); -} - -// What are we going to comment? -$path = false; - -if (empty($errors)) { - /** @var DbLayer $s2_db */ - $s2_db = $app->container->get(DbLayer::class); - - $query = array( - 'SELECT' => 'title, parent_id, url', - 'FROM' => 'articles', - 'WHERE' => 'id = ' . $id . ' AND published = 1 AND commented = 1' - ); - ($hook = s2_hook('cmnt_pre_get_page_info_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - - if (!$row = $s2_db->fetchAssoc($result)) { - $errors[] = Lang::get('no_item', 'comments'); - } else { - $path = Model::path_from_id($row['parent_id'], true); - ($hook = s2_hook('cmnt_pre_path_check')) ? eval($hook) : null; - if ($path === false) { - $errors[] = Lang::get('no_item', 'comments'); - } - } -} - -if (!empty($errors)) -{ - $error_text = '<p>'.Lang::get('Error message', 'comments').'</p><ul>'; - foreach ($errors as $error) - $error_text .= '<li>'.$error.'</li>'; - $error_text .= '</ul>'; - - $template - ->putInPlaceholder('head_title' , '❌' . Lang::get('Error')) - ->putInPlaceholder('title' , '<span class="icon-error">✖</span>' . Lang::get('Error')) - ->putInPlaceholder('text' , $error_text . ($id !== null ? '<p>' . Lang::get('Fix error', 'comments') . '</p>' : '')) - ->putInPlaceholder('id' , $id) - ->putInPlaceholder('class' , $class) - ->putInPlaceholder('commented' , $id !== null) - ->putInPlaceholder('comment_form' , compact('name', 'email', 'show_email', 'subscribed', 'text')) - ; - - $template->toHttpResponse()->send(); - - /** @var LoggerInterface $logger */ - $logger = $app->container->get(LoggerInterface::class); - $logger->notice('Comment was not saved due to errors.', [ - 'errors' => $errors, - 'id' => s2_ext_var('id'), - ]); - - die(); -} - -$link = s2_abs_link($path.'/'.urlencode($row['url'])); - -// -// Everything is ok, save and send the comment -// - -/** @var DbLayer $s2_db */ -$s2_db = $app->container->get(DbLayer::class); - -// Detect if there is a user logged in -$is_logged_in = false; -if (isset($_COOKIE[$s2_cookie_name.'_c'])) -{ - $query = array( - 'SELECT' => 'count(*)', - 'FROM' => 'users AS u', - 'JOINS' => array( - array( - 'INNER JOIN' => 'users_online AS o', - 'ON' => 'o.login = u.login' - ), - ), - 'WHERE' => 'u.email = \''.$s2_db->escape($email).'\' AND o.comment_cookie = \''.$s2_db->escape($_COOKIE[$s2_cookie_name.'_c']).'\'' - ); - ($hook = s2_hook('cmnt_pre_get_logged_in_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - - $is_logged_in = $s2_db->result($result); -} - -$is_moderate = $is_logged_in ? 0 : S2_PREMODERATION; -// Save the comment -$query = array( - 'INSERT' => 'article_id, time, ip, nick, email, show_email, subscribed, sent, shown, good, text', - 'INTO' => 'art_comments', - 'VALUES' => $id.', '.time().', \''.$s2_db->escape($_SERVER['REMOTE_ADDR']).'\', \''.$s2_db->escape($name).'\', \''.$s2_db->escape($email).'\', '.$show_email.', '.$subscribed.', '.(1 - $is_moderate).', '.(1 - $is_moderate).', 0, \''.$s2_db->escape($text).'\'' -); -($hook = s2_hook('cmnt_pre_save_comment_qr')) ? eval($hook) : null; -$s2_db->buildAndQuery($query); - -$message = s2_bbcode_to_mail($text); - -// Sending the comment to subscribers -if (!$is_moderate) -{ - $query = array( - 'SELECT' => 'id, nick, email, ip, time', - 'FROM' => 'art_comments', - 'WHERE' => 'article_id = '.$id.' AND subscribed = 1 AND shown = 1 AND email <> \''.$s2_db->escape($email).'\'' - ); - ($hook = s2_hook('cmnt_pre_get_subscribers_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - - $receivers = array(); - while ($receiver = $s2_db->fetchAssoc($result)) - $receivers[$receiver['email']] = $receiver; - - foreach ($receivers as $receiver) - { - $unsubscribe_link = S2_BASE_URL.'/comment.php?mail='.urlencode($receiver['email']).'&id='.$id.'.'.$class.'&unsubscribe='.base_convert(substr(md5($receiver['id'].$receiver['ip'].$receiver['nick'].$receiver['email'].$receiver['time']), 0, 16), 16, 36); - ($hook = s2_hook('cmnt_pre_send_mail')) ? eval($hook) : null; - s2_mail_comment($receiver['nick'], $receiver['email'], $message, $row['title'], $link, $name, $unsubscribe_link); - } -} - -// Sending the comment to moderators -$query = array( - 'SELECT' => 'login, email', - 'FROM' => 'users', - 'WHERE' => 'hide_comments = 1 AND email <> \'\'' -); -if ($is_logged_in) - $query['WHERE'] .= ' AND email <> \''.$s2_db->escape($email).'\''; -($hook = s2_hook('cmnt_pre_get_moderators_qr')) ? eval($hook) : null; -$result = $s2_db->buildAndQuery($query); -while ($mrow = $s2_db->fetchAssoc($result)) - s2_mail_moderator($mrow['login'], $mrow['email'], $message, $row['title'], $link, $name, $email); - -setcookie('comment_form_sent', $id.'.'.$class); - -if (!$is_moderate) -{ - // Redirect to the last comment - $query = array( - 'SELECT' => 'count(id)', - 'FROM' => 'art_comments', - 'WHERE' => 'article_id = '.$id.' AND shown = 1' - ); - ($hook = s2_hook('cmnt_pre_get_comment_count_qr')) ? eval($hook) : null; - $result = $s2_db->buildAndQuery($query); - $hash = $s2_db->result($result); - - ($hook = s2_hook('cmnt_pre_redirect')) ? eval($hook) : null; - - header('Location: '.s2_link($path.'/'.urlencode($row['url'])).'#'.$hash); -} -else - header('Location: '.S2_PATH.'/comment.php?go='.urlencode($link)); - -$s2_db->close(); diff --git a/index.php b/index.php index c2863f07..95aa2360 100644 --- a/index.php +++ b/index.php @@ -42,10 +42,6 @@ die; } -if (!defined('S2_COMMENTS_FUNCTIONS_LOADED')) { - require S2_ROOT . '_include/comments.php'; -} - $request = Request::createFromGlobals(); $response = $app->handle($request);