From 2b23357d11b09258dada51ea2b0932fbe6304906 Mon Sep 17 00:00:00 2001
From: Georg Ringer <georg.ringer@gmail.com>
Date: Sat, 15 Feb 2020 12:17:51 +0100
Subject: [PATCH] [TASK] Migrate LoginWarning mails to FluidEmail

Resolves: #90376
Releases: master
Change-Id: I0d7643b9bd0bd99545cd5e488a87cd381219cc42
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63250
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
---
 .../Security/LoginAttemptFailedWarning.html   |  11 ++
 .../Security/LoginAttemptFailedWarning.txt    |   9 +
 .../BackendUserAuthentication.php             | 170 ++++++++++--------
 3 files changed, 112 insertions(+), 78 deletions(-)
 create mode 100644 typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html
 create mode 100644 typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt

diff --git a/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html
new file mode 100644
index 000000000000..4cc40a8586d3
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html
@@ -0,0 +1,11 @@
+<f:layout name="SystemEmail" />
+<f:section name="Subject">TYPO3 Login Failure Warning (at {typo3.sitename})</f:section>
+<f:section name="Title">TYPO3 Login Failure Warning at {typo3.sitename}</f:section>
+<f:section name="Main">
+    <strong>There have been some attempts ({lines -> f:count()}) to login at the TYPO3 site "{typo3.sitename}" (<strong>{normalizedParams.remoteAddress}</strong>).</strong>
+    <br><br>
+    This is a dump of the failures:<br>
+<f:for each="{lines}" as="line">
+    {f:format.date(date:line.row.tstamp, format:'{typo3.formats.date} {typo3.formats.time}')}: {line.text}<br>
+</f:for>
+</f:section>
diff --git a/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt
new file mode 100644
index 000000000000..2247fe612169
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt
@@ -0,0 +1,9 @@
+<f:layout name="SystemEmail" />
+<f:section name="Subject">TYPO3 Login Failure Warning (at {typo3.sitename})</f:section>
+<f:section name="Title">TYPO3 Login Failure Warning at {typo3.sitename}</f:section>
+<f:section name="Main">There have been some attempts ({lines -> f:count()}) to login at the TYPO3 site "{typo3.sitename}" ({normalizedParams.remoteAddress}).
+
+This is a dump of the failures:
+<f:for each="{lines}" as="line">{f:format.date(date:line.row.tstamp, format:'{typo3.formats.date} {typo3.formats.time}')}: {line.text -> f:format.raw()}
+</f:for>
+</f:section>
diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
index 97d20ddda721..9cb8a2b9d27c 100644
--- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Authentication;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Doctrine\DBAL\Driver\Statement;
+use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -27,6 +29,8 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
+use TYPO3\CMS\Core\Mail\FluidEmail;
+use TYPO3\CMS\Core\Mail\Mailer;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
 use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
@@ -2333,91 +2337,101 @@ TCAdefaults.sys_note.email = ' . $this->user['email'];
      */
     public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
     {
-        if ($email) {
-            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        if (!GeneralUtility::validEmail($email)) {
+            return;
+        }
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
 
-            // Get last flag set in the log for sending
-            $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
-            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
-            $queryBuilder->select('tstamp')
-                ->from('sys_log')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'type',
-                        $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'action',
-                        $queryBuilder->createNamedParameter(4, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->gt(
-                        'tstamp',
-                        $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
-                    )
+        // Get last flag set in the log for sending
+        $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
+        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
+        $queryBuilder->select('tstamp')
+            ->from('sys_log')
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'type',
+                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->eq(
+                    'action',
+                    $queryBuilder->createNamedParameter(SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->gt(
+                    'tstamp',
+                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
                 )
-                ->orderBy('tstamp', 'DESC')
-                ->setMaxResults(1);
-            if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
-                $theTimeBack = $testRow['tstamp'];
-            }
+            )
+            ->orderBy('tstamp', 'DESC')
+            ->setMaxResults(1);
+        if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
+            $theTimeBack = $testRow['tstamp'];
+        }
 
-            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
-            $result = $queryBuilder->select('*')
-                ->from('sys_log')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'type',
-                        $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'action',
-                        $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->neq(
-                        'error',
-                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->gt(
-                        'tstamp',
-                        $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
-                    )
+        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
+        $result = $queryBuilder->select('*')
+            ->from('sys_log')
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'type',
+                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->eq(
+                    'action',
+                    $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->neq(
+                    'error',
+                    $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, \PDO::PARAM_INT)
+                ),
+                $queryBuilder->expr()->gt(
+                    'tstamp',
+                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
                 )
-                ->orderBy('tstamp')
-                ->execute();
+            )
+            ->orderBy('tstamp')
+            ->execute();
 
-            $rowCount = $queryBuilder
-                ->count('uid')
-                ->execute()
-                ->fetchColumn(0);
-            // Check for more than $max number of error failures with the last period.
-            if ($rowCount > $max) {
-                // OK, so there were more than the max allowed number of login failures - so we will send an email then.
-                $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
-                $email_body = 'There have been some attempts (' . $rowCount . ') to login at the TYPO3
-site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ').
-
-This is a dump of the failures:
-
-';
-                while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
-                    $theData = unserialize($row['log_data']);
-                    $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
-                    if ((int)$row['type'] === 255) {
-                        $text = str_replace('###IP###', $row['IP'], $text);
-                    }
-                    $email_body .= date(
-                        $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
-                        $row['tstamp']
-                    ) . ':  ' . $text;
-                    $email_body .= LF;
-                }
-                $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
-                $mail->setTo($email)->subject($subject)->text($email_body);
-                $mail->send();
-                // Logout written to log
-                $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, SystemLogErrorClassification::MESSAGE, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
+        $rowCount = $queryBuilder
+            ->count('uid')
+            ->execute()
+            ->fetchColumn(0);
+        // Check for more than $max number of error failures with the last period.
+        if ($rowCount > $max) {
+            // OK, so there were more than the max allowed number of login failures - so we will send an email then.
+            $this->sendLoginAttemptEmail($result, $email);
+            // Login failure attempt written to log
+            $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, SystemLogErrorClassification::MESSAGE, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
+        }
+    }
+
+    /**
+     * Sends out an email if the number of attempts have exceeded a limit.
+     *
+     * @param Statement $result
+     * @param string $emailAddress
+     */
+    protected function sendLoginAttemptEmail(Statement $result, string $emailAddress): void
+    {
+        $emailData = [];
+        while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+            $theData = unserialize($row['log_data'], ['allowed_classes' => false]);
+            $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
+            if ((int)$row['type'] === SystemLogType::LOGIN) {
+                $text = str_replace('###IP###', $row['IP'], $text);
             }
+            $emailData[] = [
+                'row' => $row,
+                'text' => $text
+            ];
+        }
+        $email = GeneralUtility::makeInstance(FluidEmail::class)
+            ->to($emailAddress)
+            ->setTemplate('Security/LoginAttemptFailedWarning')
+            ->assign('lines', $emailData);
+        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
+            $email->setRequest($GLOBALS['TYPO3_REQUEST']);
         }
+        GeneralUtility::makeInstance(Mailer::class)->send($email);
     }
 
     /**
-- 
GitLab