shell bypass 403
<?php /** * @package admintools * @copyright Copyright (c)2010-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Component\AdminTools\Administrator\Helper; defined('_JEXEC') or die; use Exception; use Joomla\CMS\Factory; use Joomla\CMS\Mail\MailTemplate; use Joomla\CMS\User\User; use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseInterface; /** * Manage and send emails with Joomla's email templates component */ abstract class TemplateEmails { /** * Email templates known to Admin Tools. */ private const EMAIL_DEFINITIONS = [ 'com_admintools.troubleshooting' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_TROUBLESHOOTING_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_TROUBLESHOOTING_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_TROUBLESHOOTING_BODY_HTML', 'variables' => [ 'USERNAME', 'ACTION', 'SITENAME', 'TROUBLESHOOTING_URLS', 'TROUBLESHOOTING_URLS_HTML', ], ], 'com_admintools.configmonitor' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_CONFIGMONITOR_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_CONFIGMONITOR_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_CONFIGMONITOR_BODY_HTML', 'variables' => [ 'AREA', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.userreactivate' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_USER_REACTIVATE_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_USER_REACTIVATE_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_USER_REACTIVATE_BODY_HTML', 'variables' => [ 'ACTIVATE', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.adminloginfail' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINFAIL_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINFAIL_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINFAIL_BODY_HTML', 'variables' => [ 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.adminloginsuccess' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINSUCCESS_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINSUCCESS_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_ADMINLOGINSUCCESS_BODY_HTML', 'variables' => [ 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.ipautoban' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_IPAUTOBAN_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_IPAUTOBAN_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_IPAUTOBAN_BODY_HTML', 'variables' => [ 'UNTIL', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.criticalfiles' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_BODY_HTML', 'variables' => [ 'INFO', 'INFO_HTML', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.criticalfiles_global' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_GLOBAL_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_GLOBAL_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_CRITICALFILES_GLOBAL_BODY_HTML', 'variables' => [ 'INFO', 'INFO_HTML', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.superuserslist' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_SUPERUSERSLIST_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_SUPERUSERSLIST_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_SUPERUSERSLIST_BODY_HTML', 'variables' => [ 'INFO', 'INFO_HTML', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], 'com_admintools.rescueurl' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_RESCUEURL_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_RESCUEURL_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_RESCUEURL_BODY_HTML', 'variables' => [ 'RESCUEURL', 'INFO_HTML', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', ], ], 'com_admintools.blockedrequest' => [ 'subject' => 'COM_ADMINTOOLS_EMAIL_BLOCKEDREQUEST_SUBJECT', 'bodyPlaintext' => 'COM_ADMINTOOLS_EMAIL_BLOCKEDREQUEST_BODY', 'bodyHtml' => 'COM_ADMINTOOLS_EMAIL_BLOCKEDREQUEST_BODY_HTML', 'variables' => [ 'INFO_HTML', 'USER', 'SITENAME', 'DATE', 'IP', 'URL', 'LOOKUP', 'UA', 'RESCUEINFO', 'RESCUE_TRIGGER_URL', ], ], ]; /** * Checks whether the main email template for the specific key exists in the database. * * It does NOT check if the template is up-to-date. * * @param string $key * * @return bool */ public static function hasTemplate(string $key): bool { return self::actOnTemplate($key, 'return'); } /** * Returns the number of the know templates configured in the database. * * Remember that this may include templates which are out-of-date! * * @return int */ public static function countTemplates(): int { return self::actOnTemplates('return'); } /** * Returns the number of the known email templates. * * @return int */ public static function countKnownTemplates(): int { return count(self::EMAIL_DEFINITIONS); } /** * Returns the keys of the known email templates. * * @return string[] */ public static function getKnownTemplatesKeys(): array { return array_keys(self::EMAIL_DEFINITIONS); } /** * Updates a specific email template. * * Makes sure that the main email template exists in the database. If it doesn't, it's created. If it exists and its * variables (tags), subject, body (plaintext) or body (HTML) differ it will be updated. Otherwise no further action * is taken. * * @param string $key * * @return bool */ public static function updateTemplate(string $key) { return self::actOnTemplate($key, 'fix'); } /** * Resets an email template. * * WARNING! THIS ALSO REMOVES THE USER-GENERATED EMAIL TEMPLATES FOR THIS KEY. * * @param string $key * * @return bool */ public static function resetTemplate(string $key) { return self::actOnTemplate($key, 'reset'); } /** * Update all email templates we know about. * * This operates only on the mail templates. User–generated templates are kept as-is. * * @return int */ public static function updateAllTemplates(): int { return self::actOnTemplates('fix'); } /** * Resets all email templates we know about. * * WARNING! THIS ALSO REMOVES THE USER-GENERATED EMAIL TEMPLATES FOR ALL KEYS WE KNOW. * * @return int Number of email template keys affected */ public static function resetAllTemplates(): int { return self::actOnTemplates('reset'); } /** * Removes all email templates we know about. * * WARNING! THIS ALSO REMOVES THE USER-GENERATED EMAIL TEMPLATES FOR ALL KEYS WE KNOW. * * @return int Number of email template keys affected */ public static function deleteAllTemplates(): int { return self::actOnTemplates('delete'); } /** * Sends an email using a template. * * WARNING! THIS DOES NOT CHECK IF THE TEMPLATE EXISTS. USE TemplateEmails::updateTemplate($key) FIRST. * * @param string $key The email template key to send * @param array $data The variable/tag associative array to include in the email * @param User|null $user The user to send the email to. NULL for the currently logged in user. * @param string|null $forceLanguage Force a specific language tag instead of using the user's preferences. * @param bool $throw False (default) to return false on error, True to throw the exception back * to you. * * @return bool True if the email was sent. * @throws Exception When $throw === true and there's an error sending the email */ public static function sendMail(string $key, array $data, User $user = null, string $forceLanguage = null, bool $throw = false): bool { $app = Factory::getApplication(); // If mail sending is turned off I cannot send an email if ($app->get('mailonline', 1) == 0) { return false; } if (empty($user)) { $user = $app->getIdentity(); } // if ($user->guest || $user->block || !$user->sendEmail) // { // return false; // } try { /** * We create a custom mailer, setting its priority to normal. * * Even though the Priority is nominally optional, SpamAssassin will reject emails without a priority. * That's a major WTF which even Joomla itself doesn't know about :O */ $mailer = Factory::getMailer(); $mailer->Priority = 3; $app = Factory::getApplication(); $appLang = $app->getLanguage() ?? null; $appLang = is_object($appLang) ? $appLang->getTag() : null; $userLang = $app->isClient('administrator') ? $user->getParam('administrator_language') : $user->getParam('language'); $userFrontendLang = $user->getParam('language'); $langTag = $userLang ?: $userFrontendLang ?: $appLang ?: 'en-GB'; $langTag = $forceLanguage ?: $langTag; /** * Try to get the template. Remember that Joomla looks for the specific language tag or the main template * which defines no language and falls back to translation strings. */ $template = MailTemplate::getTemplate($key, $langTag); if (empty($template)) { // Yeah, well, there's no template. I can't send the email, I'm afraid. return false; } $templateMailer = new MailTemplate($key, $langTag, $mailer); $templateMailer->addTemplateData($data); $templateMailer->addRecipient(trim($user->email), $user->name); return $templateMailer->send(); } catch (Exception $e) { if ($throw) { throw $e; } return false; } } private static function actOnTemplate(string $key, string $action = 'return'): bool { /** * Note that we are only checking the email template WITHOUT a language. This is considered the "default" email * template from which all the localised email templates are generated. We only care if that email template * exists and is up to date. We don't mess with the user-defined email templates, ever! */ try { /** @var DatabaseDriver $db */ $db = Factory::getContainer()->get(DatabaseInterface::class); $query = $db->getQuery(true); $query->select('*') ->from($db->quoteName('#__mail_templates')) ->where($db->quoteName('template_id') . ' = :key') ->where($db->quoteName('language') . ' = ' . $db->quote('')) ->order($db->quoteName('language') . ' DESC') ->bind(':key', $key); $templateInDB = $db->setQuery($query)->loadAssoc() ?: []; $hasTemplate = !empty($templateInDB); } catch (\Exception $e) { $templateInDB = []; $hasTemplate = false; } $knownTemplate = array_key_exists($key, self::EMAIL_DEFINITIONS); $action = strtolower($action); switch (strtolower($action)) { // Ensures a template exists and its definition is up-to-date case 'fix': if (!$knownTemplate) { return false; } // The template does not exist in the database. Create it. if (!$hasTemplate) { $record = self::EMAIL_DEFINITIONS[$key]; self::createTemplate($key, $record['subject'], $record['bodyPlaintext'], $record['variables'], $record['bodyHtml'] ?? ''); return true; } $record = self::EMAIL_DEFINITIONS[$key]; // Do I need to update the record? We check the variables, subject and the plaintext and HTML bodies. try { $params = json_decode($templateInDB['params'], true); $variablesInDB = array_map('strtoupper', (array) $params['tags'][0] ?? []); $variablesKnown = array_map('strtoupper', $record['variables'] ?? []); $isIdentical = empty(array_diff($variablesKnown, $variablesInDB)); $isIdentical = $isIdentical && ($templateInDB['subject'] == $record['subject']); $isIdentical = $isIdentical && ($templateInDB['body'] == $record['bodyPlaintext']); $isIdentical = $isIdentical && ($templateInDB['htmlbody'] == $record['bodyHtml']); } catch (\Exception $e) { // The template is corrupt. We will reset it. return self::actOnTemplate($key, 'reset'); } // The template in the DB is up-to-date. Bye-bye! if ($isIdentical) { return true; } // There were differences. Let's update the template. self::updateTemplateInDB($key, $record['subject'], $record['bodyPlaintext'], $record['variables'], $record['bodyHtml'] ?? ''); return true; break; // Forcibly update a template if exists case 'update': if (!$knownTemplate) { return false; } if (!$hasTemplate) { return true; } $record = self::EMAIL_DEFINITIONS[$key]; self::updateTemplateInDB($key, $record['subject'], $record['bodyPlaintext'], $record['variables'], $record['bodyHtml'] ?? ''); return true; break; // Forcibly reset a template case 'reset': if (!$knownTemplate) { return false; } if ($hasTemplate) { MailTemplate::deleteTemplate($key); } $record = self::EMAIL_DEFINITIONS[$key]; self::createTemplate($key, $record['subject'], $record['bodyPlaintext'], $record['variables'], $record['bodyHtml'] ?? ''); return true; break; // Only return whether a template exists case 'return': default: return $hasTemplate; break; } } private static function actOnTemplates(string $action = 'return'): int { $count = 0; foreach (array_keys(self::EMAIL_DEFINITIONS) as $key) { if ($action === 'delete') { MailTemplate::deleteTemplate($key); continue; } if (self::actOnTemplate($key, $action)) { $count++; } } return $count; } /** * Fork of MailTemplate::createTemplate WHICH ACTUALLY WORKS WITHOUT THROWING ERRORS. * * 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 7.0.0 */ private static function createTemplate(string $key, string $subject, string $body, array $tags, string $htmlbody = ''): bool { /** @var DatabaseDriver $db */ $db = Factory::getContainer()->get(DatabaseInterface::class); $template = new \stdClass; $template->template_id = $key; $template->language = ''; $template->subject = $subject; $template->body = $body; $template->htmlbody = $htmlbody; $template->attachments = ''; $template->extension = explode('.', $key, 2)[0]; $params = new \stdClass; $params->tags = $tags; $template->params = json_encode($params); return $db->insertObject('#__mail_templates', $template); } /** * Fork of MailTemplate::updateTemplate WHICH ACTUALLY WORKS WITHOUT THROWING ERRORS. * * 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 7.0.0 */ private static function updateTemplateInDB($key, $subject, $body, $tags, $htmlbody = '') { /** @var DatabaseDriver $db */ $db = Factory::getContainer()->get(DatabaseInterface::class); $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']); } }