Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/classes/Core/Alert.php → core/classes/Alerts/Alert.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public static function send(int $userId, string $title, ?string $content, ?strin
'created' => date('U'),
'bypass_purify' => $skipPurify,
]);

// TODO: AlertCreatedEvent for discord to listen to to DM the user on discord?
}

/**
Expand Down
14 changes: 14 additions & 0 deletions core/classes/Alerts/AlertTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

class AlertTemplate
{
public function __construct(
public LanguageKey|string $title,
public LanguageKey|string|null $content = null,
public ?string $link = null,
) {
if ($this->link === null && $this->content === null) {
throw new InvalidArgumentException('Either link or content must be provided');
}
}
}
10 changes: 5 additions & 5 deletions core/classes/Database/DB.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,12 @@ public function insert(string $table, array $fields = []): bool
/**
* Perform an UPDATE query on a table.
*
* @param string $table The table to update.
* @param mixed $where The where clause. If not an array, it will be used for "id" column lookup.
* @param array $fields Array of data in "column => value" format to update.
* @return bool Whether an error occurred or not.
* @param string $table The table to update.
* @param array|int $where The where clause. If not an array, it will be used for "id" column lookup.
* @param array $fields Array of data in "column => value" format to update.
* @return bool Whether an error occurred or not.
*/
public function update(string $table, $where, array $fields): bool
public function update(string $table, array|int $where, array $fields): bool
{
$set = '';
$x = 1;
Expand Down
108 changes: 44 additions & 64 deletions core/classes/Core/Email.php → core/classes/Emails/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,61 @@ class Email
{
public const EMAIL_MAX_LENGTH = 75000;

public const REGISTRATION = 1;
public const FORGOT_PASSWORD = 3;
public const API_REGISTRATION = 4;
public const FORUM_TOPIC_REPLY = 5;
public const MASS_MESSAGE = 6;
public const TEST_EMAIL = 'TestEmail';
public const MASS_MESSAGE = 'MassMessage';

/**
* @var array<string, string> Placeholders for email templates
*/
private static array $_message_placeholders = [];
public static function send(User $recipient, EmailTemplate $emailTemplate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to keep support for setting email, Such as Store and Forms module will send emails to quests based on the email they entered

{
$languageCode = DB::getInstance()->get('languages', ['id', '=', $recipient->data()->language_id])->first()->short_code;

return self::sendInternal(
str_replace('EmailTemplate', '', $emailTemplate::class),
$recipient,
$emailTemplate->subject()->translate($languageCode),
$emailTemplate->renderContent($languageCode)
);
}

public static function sendRaw(string $mailer, User $recipient, string $subject, string $content)
{
return self::sendInternal($mailer, $recipient, $subject, $content);
}

/**
* Send an email.
* Internal helper method to handle common email sending logic.
*
* @param array $recipient Array containing `'email'` and `'name'` strings for the recipient of the email.
* @param string $subject Subject of the email.
* @param string $message Message of the email.
* @param array|null $reply_to Array containing `'email'` and `'name'` strings for the reply-to address,
* if not provided the default setting will be used.
* @return bool|array Returns true if email sent, otherwise returns an array containing the error.
* @param string $mailer Email mailer identifier
* @param User $recipient Recipient user object
* @param string $subject Email subject
* @param string $content Email content
* @return bool|array Returns true if email sent, otherwise returns an array containing the error
*/
public static function send(array $recipient, string $subject, string $message, ?array $reply_to = null)
private static function sendInternal(string $mailer, User $recipient, string $subject, string $content)
{
$email = [
'to' => $recipient,
'subject' => $subject,
'message' => $message,
'replyto' => $reply_to ?? self::getReplyTo(),
'to' => [
'email' => $recipient->data()->email,
'name' => $recipient->getDisplayname(),
],
'subject' => SITE_NAME . ' - ' . $subject,
'message' => $content,
'replyto' => self::getReplyTo(),
];

if (Settings::get('phpmailer') == '1') {
return self::sendMailer($email);
$result = Settings::get('phpmailer') == '1'
? self::sendMailer($email)
: self::sendPHP($email);

if (isset($result['error'])) {
DB::getInstance()->insert('email_errors', [
'mailer' => $mailer,
'content' => $result['error'],
'at' => date('U'),
'user_id' => $recipient->data()->id,
]);
}

return self::sendPHP($email);
return $result;
}

/**
Expand Down Expand Up @@ -108,12 +128,10 @@ private static function sendPHP(array $email)
private static function sendMailer(array $email)
{
try {
// Initialise PHPMailer
$mail = new PHPMailer(true);

$mail->IsSMTP();
$mail->SMTPDebug = SMTP::DEBUG_OFF;
$mail->Debugoutput = 'html';
$mail->CharSet = PHPMailer::CHARSET_UTF8;
$mail->Encoding = PHPMailer::ENCODING_BASE64;
$mail->Timeout = 15;
Expand Down Expand Up @@ -156,42 +174,4 @@ private static function sendMailer(array $email)
];
}
}

/**
* Add a custom placeholder/variable for email messages.
*
* @param string $key The key to use for the placeholder, should be enclosed in square brackets.
* @param string|Closure(Language, string): string $value The value to replace the placeholder with.
*/
public static function addPlaceholder(string $key, $value): void
{
self::$_message_placeholders[$key] = $value;
}

/**
* Format an email template and replace placeholders.
*
* @param string $email Name of email to format.
* @param Language $viewing_language Instance of Language class to use for translations.
* @return string Formatted email.
*/
public static function formatEmail(string $email, Language $viewing_language): string
{
$placeholders = array_keys(self::$_message_placeholders);

$placeholder_values = [];
foreach (self::$_message_placeholders as $value) {
if (is_callable($value)) {
$placeholder_values[] = $value($viewing_language, $email);
} else {
$placeholder_values[] = $value;
}
}

return str_replace(
$placeholders,
$placeholder_values,
file_get_contents(implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', TEMPLATE, 'email', $email . '.html']))
);
}
}
78 changes: 78 additions & 0 deletions core/classes/Emails/EmailTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

abstract class EmailTemplate
{
/**
* @var array<string, LanguageKey|string> Placeholders for this email template
*/
private array $_placeholders = [];

public function __construct()
{
$this->addPlaceholder('[Sitename]', Output::getClean(SITE_NAME));
$this->addPlaceholder('[Greeting]', new LanguageKey('emails', 'greeting'));
$this->addPlaceholder('[Thanks]', new LanguageKey('emails', 'thanks'));
}

/**
* Returns the snake_case representation of the email template name,
* derived from the class name with "EmailTemplate" removed.
* For example: RegisterEmailTemplate -> "register", ForgotPasswordEmailTemplate -> "forgot_password".
*/
private function name(): string
{
$baseName = str_replace('EmailTemplate', '', static::class);

return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $baseName));
}

abstract public function subject(): LanguageKey|string;

/**
* Add a custom placeholder/variable for email messages.
*
* @param string $key The key to use for the placeholder, should be enclosed in square brackets.
* @param string|Closure(Language, string): string $value The value to replace the placeholder with.
*/
final public function addPlaceholder(string $key, $value): void
{
$this->_placeholders[$key] = $value;
}

final public function renderContent(string $languageCode): string
{
$placeholderKeys = array_keys($this->_placeholders);
$placeholderValues = [];

foreach ($this->_placeholders as $placeholder) {
if ($placeholder instanceof LanguageKey) {
$placeholderValues[] = $placeholder->translate($languageCode);
} else {
$placeholderValues[] = $placeholder;
}
}

return str_replace(
$placeholderKeys,
$placeholderValues,
file_get_contents($this->getPath()),
);
}

private function getPath(): string
{
$name = $this->name();

$customPath = implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', TEMPLATE, 'email', $name . '.html']);
if (file_exists($customPath)) {
return $customPath;
}

$defaultPath = implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'custom', 'templates', 'DefaultRevamp', 'email', $name . '.html']);
if (file_exists($defaultPath)) {
return $defaultPath;
}

throw new Exception('Email template not found: ' . $name);
}
}
10 changes: 3 additions & 7 deletions core/classes/Misc/MentionsParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static function parse(int $author_id, string $content): string
*
* @return string Parsed post content.
*/
public static function parseAndNotify(int $author_id, string $content, string $url, string $notificationType, LanguageKey $notificationTitle): string
public static function parseAndNotify(int $author_id, string $content, string $notificationType, AlertTemplate $notificationAlertTemplate, EmailTemplate $notificationEmailTemplate): string
{
$receipients = self::getRecipients($content, $author_id);

Expand All @@ -46,14 +46,10 @@ public static function parseAndNotify(int $author_id, string $content, string $u

$notification = new Notification(
$notificationType,
$notificationTitle,
// TODO: emails content - right now it will be plaintext and not use a template
$content,
$notificationAlertTemplate,
$notificationEmailTemplate,
$notificationRecipients,
$author_id,
null,
false,
$url,
);

$notification->send();
Expand Down
19 changes: 11 additions & 8 deletions core/classes/Misc/Report.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ public static function create(Language $language, User $user_reporting, User $re

// Alert moderators
$moderators = DB::getInstance()->query('SELECT DISTINCT(nl2_users.id) AS id FROM nl2_users LEFT JOIN nl2_users_groups ON nl2_users.id = nl2_users_groups.user_id WHERE group_id IN (SELECT id FROM nl2_groups WHERE permissions LIKE \'%"modcp.reports":1%\')')->results();
$link = rtrim(URL::getSelfURL(), '/') . URL::build('/panel/users/reports/', 'id=' . $id);
$notification = new Notification(
'report',
new LanguageKey('moderator', 'report_alert'),
new LanguageKey('moderator', 'report_email', [
'linkStart' => '<a href="' . rtrim(URL::getSelfURL(), '/') . URL::build('/panel/users/reports/', 'id=' . $id) . '">',
'linkEnd' => '</a>',
]),
new AlertTemplate(
new LanguageKey('moderator', 'report_alert'),
null,
$link,
),
new ReportCreatedEmailTemplate(
$link,
$reported_user,
$user_reporting,
),
array_map(fn ($moderator) => $moderator->id, $moderators),
$user_reporting->data()->id,
null,
false,
rtrim(URL::getSelfURL(), '/') . URL::build('/panel/users/reports/', 'id=' . $id),
);
$notification->send();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class ConvertEmailErrorTypeToString extends AbstractMigration
{
private const CONVERSION_MAP = [
1 => RegisterEmailTemplate::class,
3 => ForgotPasswordEmailTemplate::class,
5 => ForumTopicReplyEmailTemplate::class,
6 => MassMessageEmailTemplate::class,
];

public function change(): void
{
$this->table('nl2_email_errors')
->renameColumn('type', 'mailer')
->changeColumn('mailer', 'string', ['limit' => 255])
->update();

$email_errors = DB::getInstance()->query('SELECT * FROM nl2_email_errors')->results();

foreach ($email_errors as $error) {
$type = $error->mailer;
if (isset(self::CONVERSION_MAP[$type])) {
DB::getInstance()->update('email_errors', $error->id, [
'mailer' => self::CONVERSION_MAP[$type],
]);
} else {
DB::getInstance()->update('email_errors', $error->id, [
'mailer' => 'unknown',
]);
}
}
}
}
1 change: 0 additions & 1 deletion custom/panel_templates/Default/core/emails.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
{if isset($MASS_MESSAGE_LINK)}
<a href="{$MASS_MESSAGE_LINK}" class="btn btn-primary">{$MASS_MESSAGE}</a>
{/if}
<a href="{$EDIT_EMAIL_MESSAGES_LINK}" class="btn btn-primary">{$EDIT_EMAIL_MESSAGES}</a>
<a href="{$EMAIL_ERRORS_LINK}" class="btn btn-primary">{$EMAIL_ERRORS}</a>
<a href="{$SEND_TEST_EMAIL_LINK}" class="btn btn-info">{$SEND_TEST_EMAIL}</a>

Expand Down
Loading