[FEATURE] Extbase-based Frontend Login Form 00/61900/39
authorJan Stockfisch <typo3@jan-stockfisch.de>
Mon, 7 Oct 2019 07:52:02 +0000 (09:52 +0200)
committerSusanne Moog <look@susi.dev>
Thu, 28 Nov 2019 07:28:33 +0000 (08:28 +0100)
A new Extbase-based plugin is added to TYPO3's Extension "felogin"
which can be used as a toggle with custom templates based on Fluid,
instead of marker-based templates.

A new feature toggle is added to the Settings module for Site Administrators
to switch to the newly added felogin extbase plugin, or continuing using
the pibase plugin, e.g. for upgrading purposes.

Resolves: #84262
Releases: master
Change-Id: I9d281912373a078e0403f52b27483dd3e04785f7
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61900
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Alexander Schnitzler <review.typo3.org@alexanderschnitzler.de>
Reviewed-by: Susanne Moog <look@susi.dev>
64 files changed:
composer.json
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
typo3/sysext/core/Configuration/FactoryConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Feature-88102-FrontendLoginViaFluidAndExtbase.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-88110-FeloginExtbasePasswordRecovery.rst [new file with mode: 0644]
typo3/sysext/felogin/Classes/Configuration/IncompleteConfigurationException.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Configuration/RecoveryConfiguration.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Configuration/RedirectConfiguration.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
typo3/sysext/felogin/Classes/Controller/LoginController.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Controller/PasswordRecoveryController.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserGroupRepository.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserRepository.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Event/LoginConfirmedEvent.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Event/ModifyLoginFormViewEvent.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Event/PasswordChangeEvent.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Event/SendRecoveryEmailEvent.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Helper/TreeUidListProvider.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Hooks/CmsLayout.php
typo3/sysext/felogin/Classes/Redirect/RedirectHandler.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Redirect/RedirectMode.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Redirect/RedirectModeHandler.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Redirect/ServerRequestHandler.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Service/RecoveryService.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Service/RecoveryServiceInterface.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Service/UserService.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Service/ValidatorResolverService.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Updates/MigrateFeloginPlugins.php
typo3/sysext/felogin/Classes/Updates/MigrateFeloginPluginsCtype.php [new file with mode: 0644]
typo3/sysext/felogin/Classes/Validation/RedirectUrlValidator.php
typo3/sysext/felogin/Configuration/Services.yaml
typo3/sysext/felogin/Configuration/TCA/Overrides/tt_content.php
typo3/sysext/felogin/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig
typo3/sysext/felogin/Configuration/TsConfig/Page/PiBase/Mod/Wizards/NewContentElement.tsconfig [new file with mode: 0644]
typo3/sysext/felogin/Configuration/TypoScript/PiBase/constants.typoscript [new file with mode: 0644]
typo3/sysext/felogin/Configuration/TypoScript/PiBase/setup.typoscript [new file with mode: 0644]
typo3/sysext/felogin/Configuration/TypoScript/constants.typoscript
typo3/sysext/felogin/Configuration/TypoScript/setup.typoscript
typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.txt [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Language/Database.xlf
typo3/sysext/felogin/Resources/Private/Language/locallang.xlf
typo3/sysext/felogin/Resources/Private/Partials/CookieWarning.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Partials/RenderLabelOrMessage.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Partials/ValidationErrors.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Templates/Login/Login.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Templates/Login/Logout.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Templates/Login/Overview.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/Recovery.html [new file with mode: 0644]
typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/ShowChangePassword.html [new file with mode: 0644]
typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserGroupRepositoryTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserRepositoryTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Functional/Fixtures/fe_groups.xml [new file with mode: 0644]
typo3/sysext/felogin/Tests/Functional/Fixtures/fe_users.xml [new file with mode: 0644]
typo3/sysext/felogin/Tests/Functional/Tca/ContentVisibleFieldsTest.php
typo3/sysext/felogin/Tests/Unit/Configuration/RecoveryConfigurationTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Unit/Redirect/RedirectHandlerTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Unit/Service/RecoveryServiceTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Unit/Service/TreeUidListProviderTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Unit/Service/ValidatorResolverServiceTest.php [new file with mode: 0644]
typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php
typo3/sysext/felogin/composer.json
typo3/sysext/felogin/ext_localconf.php

index 3d41a8a..e56e5f1 100644 (file)
                        "TYPO3\\CMS\\Extbase\\": "typo3/sysext/extbase/Classes/",
                        "TYPO3\\CMS\\Extensionmanager\\": "typo3/sysext/extensionmanager/Classes/",
                        "TYPO3\\CMS\\Felogin\\": "typo3/sysext/felogin/Classes/",
+                       "TYPO3\\CMS\\FrontendLogin\\": "typo3/sysext/felogin/Classes/",
                        "TYPO3\\CMS\\Filelist\\": "typo3/sysext/filelist/Classes/",
                        "TYPO3\\CMS\\Fluid\\": "typo3/sysext/fluid/Classes/",
                        "TYPO3\\CMS\\FluidStyledContent\\": "typo3/sysext/fluid_styled_content/Classes/",
                        "TYPO3\\CMS\\Extbase\\Tests\\": "typo3/sysext/extbase/Tests/",
                        "TYPO3\\CMS\\Extensionmanager\\Tests\\": "typo3/sysext/extensionmanager/Tests/",
                        "TYPO3\\CMS\\Felogin\\Tests\\": "typo3/sysext/felogin/Tests/",
+                       "TYPO3\\CMS\\FrontendLogin\\Tests\\": "typo3/sysext/felogin/Tests/",
                        "TYPO3\\CMS\\Filemetadata\\Tests\\": "typo3/sysext/filemetadata/Tests/",
                        "TYPO3\\CMS\\Fluid\\Tests\\": "typo3/sysext/fluid/Tests/",
                        "TYPO3\\CMS\\FluidStyledContent\\Tests\\": "typo3/sysext/fluid_styled_content/Tests/",
index 39df423..1538f26 100644 (file)
@@ -78,6 +78,7 @@ return [
             'security.frontend.keepSessionDataOnLogout' => false,
             'rearrangedRedirectMiddlewares' => false,
             'betaTranslationServer' => false,
+            'felogin.extbase' => false,
         ],
         'createGroup' => '',
         'sitename' => 'TYPO3',
index 65697ae..77e238b 100644 (file)
@@ -220,6 +220,9 @@ SYS:
               betaTranslationServer:
                 type: bool
                 description: 'If on, the new translation server is used which is filled by exports of https://crowdin.com/project/typo3-cms. This setting will be removed as soon as the new integration is stable.'
+              felogin.extbase:
+                type: bool
+                description: 'If activated, and if extension "felogin" is loaded, extbase based code will be used instead of pibase version'
         availablePasswordHashAlgorithms:
             type: array
             description: 'A list of available password hash mechanisms. Extensions may register additional mechanisms here. This is usually not extended in LocalConfiguration.php.'
index e0e6177..5ed31d3 100644 (file)
@@ -22,6 +22,7 @@ return [
         'features' => [
             'unifiedPageTranslationHandling' => true,
             'rearrangedRedirectMiddlewares' => true,
+            'felogin.extbase' => true,
         ],
     ],
 ];
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-88102-FrontendLoginViaFluidAndExtbase.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-88102-FrontendLoginViaFluidAndExtbase.rst
new file mode 100644 (file)
index 0000000..92b69ff
--- /dev/null
@@ -0,0 +1,30 @@
+.. include:: ../../Includes.txt
+
+===========================================================
+Feature: #88102 - Frontend Login Form Via Fluid And Extbase
+===========================================================
+
+See :issue:`88102`
+
+Description
+===========
+
+The system extension "felogin" now has two plugins. The original plugin which was built via the "PiBase" Plugin
+Framework and Marker-based templates continues to work, but is superseded with a new Extbase- and Fluid-based plugin,
+allowing to customize templates just like any other modern plugin.
+
+A new feature toggle is introduced to switch between the "PiBase" plugin and the Extbase plugin, which can be switched
+in the Install Tool. For existing installations, the default "PiBased" plugin is activated.
+
+Migration
+=========
+
+To migrate existing Frontend Login Form plugins an update wizard called "Migrate felogin plugins to use extbase CType"
+is provided. The wizard can also be used to switch back from the Extbase version to PiBase.
+
+When using extbase, fluid templates are used to display the depending content. These can be overridden via TypoScript
+for customization. All existing templates are found in
+
+EXT:felogin/Resources/Private/Templates/{Login,PasswordRecovery}
+
+.. index:: Frontend, LocalConfiguration, ext:felogin
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-88110-FeloginExtbasePasswordRecovery.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-88110-FeloginExtbasePasswordRecovery.rst
new file mode 100644 (file)
index 0000000..6fb9d7d
--- /dev/null
@@ -0,0 +1,55 @@
+.. include:: ../../Includes.txt
+
+===================================================
+Feature: #88110 - Felogin extbase password recovery
+===================================================
+
+See :issue:`88110`
+
+Description
+===========
+
+As part of the felogin extbase plugin, a password recovery form has been added.
+
+FE users are able to request a password change via email. A mail with a forgot hash will be send to the requesting user.
+If that hash is found valid a form reset password form is shown. If all validators are met the users password will be updated.
+
+There is a way to define and override default validators. Configured as default are two validators: NotEmptyValidator and StringLengthValidator.
+
+They can be overridden by overwriting :ts:`plugin.tx_felogin_login.settings.passwordValidators`.
+Default is as follows:
+
+.. code-block:: typoscript
+
+   passwordValidators {
+      10 = TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+      20 {
+         className = TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator
+         options {
+            minimum = {$styles.content.loginform.newPasswordMinLength}
+         }
+      }
+   }
+
+A custom configuration can look like this:
+
+.. code-block:: typoscript
+
+   passwordValidators {
+      10 = TYPO3\CMS\Extbase\Validation\Validator\AlphanumericValidator
+      20 {
+         className = TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator
+         options {
+            minimum = {$styles.content.loginform.newPasswordMinLength}
+            maximum = 32
+         }
+      }
+      30 = \Vendor\MyExt\Validation\Validator\MyCustomPasswordPolicyValidator
+   }
+
+Impact
+======
+
+No direct impact. Only used, if feature toggle "felogin.extbase" is explicitly turned on.
+
+.. index:: Database, FlexForm, Fluid, Frontend, TypoScript, ext:felogin
diff --git a/typo3/sysext/felogin/Classes/Configuration/IncompleteConfigurationException.php b/typo3/sysext/felogin/Classes/Configuration/IncompleteConfigurationException.php
new file mode 100644 (file)
index 0000000..1260eaf
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class IncompleteConfigurationException extends \Exception
+{
+}
diff --git a/typo3/sysext/felogin/Classes/Configuration/RecoveryConfiguration.php b/typo3/sysext/felogin/Classes/Configuration/RecoveryConfiguration.php
new file mode 100644 (file)
index 0000000..263e266
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\Mime\Address;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
+use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException;
+use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class RecoveryConfiguration implements LoggerAwareInterface
+{
+    use LoggerAwareTrait;
+
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var string
+     */
+    private $forgotHash;
+
+    /**
+     * @var string
+     */
+    private $htmlMailTemplatePath;
+
+    /**
+     * @var string
+     */
+    private $plainMailTemplatePath;
+
+    /**
+     * @var Address|null
+     */
+    private $replyTo;
+
+    /**
+     * @var Address
+     */
+    private $sender;
+
+    /**
+     * @var array
+     */
+    private $settings;
+
+    /**
+     * @var StandaloneView
+     */
+    private $plainMailTemplate;
+
+    /**
+     * @var StandaloneView
+     */
+    private $htmlMailTemplate;
+
+    /**
+     * @var int
+     */
+    private $timestamp;
+
+    /**
+     * @param Context $context
+     * @param ConfigurationManager $configurationManager
+     * @param Random $random
+     * @param HashService $hashService
+     * @throws IncompleteConfigurationException
+     * @throws InvalidConfigurationTypeException
+     */
+    public function __construct(
+        Context $context,
+        ConfigurationManager $configurationManager,
+        Random $random,
+        HashService $hashService
+    ) {
+        $this->context = $context;
+        $this->settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS);
+        $this->forgotHash = $this->getLifeTimeTimestamp() . '|' . $this->generateHash($random, $hashService);
+        $this->resolveFromTypoScript();
+    }
+
+    /**
+     * Return true if a html mail template path is configured otherwise false.
+     *
+     * @return bool
+     */
+    public function hasHtmlMailTemplate(): bool
+    {
+        if (empty($this->htmlMailTemplatePath)) {
+            $this->logger->warning(
+                'Key "plugin.tx_felogin_login.settings.email_htmlTemplatePath" is empty or unset.',
+                [$this]
+            );
+        }
+        return (bool)$this->htmlMailTemplatePath;
+    }
+
+    /**
+     * Returns the forgot hash.
+     *
+     * @return string
+     */
+    public function getForgotHash(): string
+    {
+        return $this->forgotHash;
+    }
+
+    /**
+     * Returns the html template view if a path is configured otherwise null.
+     *
+     * @return StandaloneView|null
+     */
+    public function getHtmlMailTemplate(): ?StandaloneView
+    {
+        if ($this->htmlMailTemplate === null && $this->htmlMailTemplatePath) {
+            $mailTemplate = GeneralUtility::makeInstance(StandaloneView::class);
+            $mailTemplate->setTemplatePathAndFilename($this->htmlMailTemplatePath);
+            $this->htmlMailTemplate = $mailTemplate;
+        }
+
+        return $this->htmlMailTemplate;
+    }
+
+    /**
+     * Returns TTL timestamp of the forgot hash
+     *
+     * @return int
+     */
+    public function getLifeTimeTimestamp(): int
+    {
+        if ($this->timestamp === null) {
+            $lifetimeInHours = $this->settings['forgotLinkHashValidTime'] ?: 12;
+            $currentTimestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
+            $this->timestamp = $currentTimestamp + 3600 * $lifetimeInHours;
+        }
+
+        return $this->timestamp;
+    }
+
+    /**
+     * Returns plain template view.
+     *
+     * @return StandaloneView
+     * @throws IncompleteConfigurationException if no path is configured
+     */
+    public function getPlainMailTemplate(): StandaloneView
+    {
+        if ($this->plainMailTemplate === null) {
+            $mailTemplate = GeneralUtility::makeInstance(StandaloneView::class);
+            $mailTemplate->setTemplatePathAndFilename($this->plainMailTemplatePath);
+            $this->plainMailTemplate = $mailTemplate;
+        }
+
+        return $this->plainMailTemplate;
+    }
+
+    /**
+     * Returns reply-to address if configured otherwise null.
+     *
+     * @return Address|null
+     */
+    public function getReplyTo(): ?Address
+    {
+        return $this->replyTo;
+    }
+
+    /**
+     * Returns the sender. Normally the current typo3 installation.
+     *
+     * @return Address
+     */
+    public function getSender(): Address
+    {
+        return $this->sender;
+    }
+
+    protected function generateHash(Random $random, HashService $hashService): string
+    {
+        $randomString = $random->generateRandomHexString(16);
+
+        return $hashService->generateHmac($randomString);
+    }
+
+    protected function resolveFromTypoScript(): void
+    {
+        $fromAddress = $this->settings['email_from'] ?: $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'];
+        if (empty($fromAddress)) {
+            throw new IncompleteConfigurationException(
+                'Either "$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'defaultMailFromAddress\']" or extension key "plugin.tx_felogin_login.settings.email_from" cannot be empty!',
+                1573825624
+            );
+        }
+        $fromName = $this->settings['email_fromName'] ?: $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'];
+        if (empty($fromName)) {
+            throw new IncompleteConfigurationException(
+                'Either "$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'defaultMailFromName\']" or extension key "plugin.tx_felogin_login.settings.email_fromName" cannot be empty!',
+                1573825625
+            );
+        }
+        $this->sender = new Address($fromAddress, $fromName);
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress'])) {
+            if ($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToName']) {
+                $this->replyTo = new Address(
+                    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress'],
+                    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToName']
+                );
+            } else {
+                $this->replyTo = new Address(
+                    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailReplyToAddress']
+                );
+            }
+        }
+        $this->plainMailTemplatePath = $this->settings['email_plainTemplatePath'];
+        if (empty($this->plainMailTemplatePath)) {
+            throw new IncompleteConfigurationException(
+                'Key "plugin.tx_felogin_login.settings.email_plainTemplatePath" cannot be empty!',
+                1562665945
+            );
+        }
+        $this->htmlMailTemplatePath = $this->settings['email_htmlTemplatePath'] ?? '';
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Configuration/RedirectConfiguration.php b/typo3/sysext/felogin/Classes/Configuration/RedirectConfiguration.php
new file mode 100644 (file)
index 0000000..fd7e880
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * A class that holds and manages all states relevant for handling redirects
+ *
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+final class RedirectConfiguration
+{
+    /**
+     * @var array
+     */
+    private $modes;
+
+    /**
+     * @var string
+     */
+    private $firstMode;
+
+    /**
+     * @var int
+     */
+    private $pageOnLogin;
+
+    /**
+     * @var string
+     */
+    private $domains;
+
+    /**
+     * @var int
+     */
+    private $pageOnLoginError;
+
+    /**
+     * @var int
+     */
+    private $pageOnLogout;
+
+    public function __construct(string $mode, string $firstMode, int $pageOnLogin, string $domains, int $pageOnLoginError, int $pageOnLogout)
+    {
+        $this->modes = GeneralUtility::trimExplode(',', $mode ?? '', true);
+        $this->firstMode = $firstMode;
+        $this->pageOnLogin = $pageOnLogin;
+        $this->domains = $domains;
+        $this->pageOnLoginError = $pageOnLoginError;
+        $this->pageOnLogout = $pageOnLogout;
+    }
+
+    /**
+     * @return array
+     */
+    public function getModes(): array
+    {
+        return $this->modes;
+    }
+
+    /**
+     * @return string
+     */
+    public function getFirstMode(): string
+    {
+        return $this->firstMode;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPageOnLogin(): int
+    {
+        return $this->pageOnLogin;
+    }
+
+    /**
+     * @return string
+     */
+    public function getDomains(): string
+    {
+        return $this->domains;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPageOnLoginError(): int
+    {
+        return $this->pageOnLoginError;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPageOnLogout(): int
+    {
+        return $this->pageOnLogout;
+    }
+}
index c5e7fcd..6ea5b75 100644 (file)
@@ -28,8 +28,8 @@ use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MailUtility;
-use TYPO3\CMS\Felogin\Validation\RedirectUrlValidator;
 use TYPO3\CMS\Frontend\Plugin\AbstractPlugin;
+use TYPO3\CMS\FrontendLogin\Validation\RedirectUrlValidator;
 
 /**
  * Plugin 'Website User Login' for the 'felogin' extension.
diff --git a/typo3/sysext/felogin/Classes/Controller/LoginController.php b/typo3/sysext/felogin/Classes/Controller/LoginController.php
new file mode 100644 (file)
index 0000000..089539f
--- /dev/null
@@ -0,0 +1,285 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Authentication\LoginType;
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
+use TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration;
+use TYPO3\CMS\FrontendLogin\Event\LoginConfirmedEvent;
+use TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent;
+use TYPO3\CMS\FrontendLogin\Helper\TreeUidListProvider;
+use TYPO3\CMS\FrontendLogin\Redirect\RedirectHandler;
+use TYPO3\CMS\FrontendLogin\Redirect\ServerRequestHandler;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+
+/**
+ * Used for plugin login
+ */
+class LoginController extends ActionController
+{
+    /**
+     * @var string
+     */
+    public const MESSAGEKEY_DEFAULT = 'welcome';
+
+    /**
+     * @var string
+     */
+    public const MESSAGEKEY_ERROR = 'error';
+
+    /**
+     * @var string
+     */
+    public const MESSAGEKEY_LOGOUT = 'logout';
+
+    /**
+     * @var RedirectHandler
+     */
+    protected $redirectHandler;
+
+    /**
+     * @var string
+     */
+    protected $loginType = '';
+
+    /**
+     * @var TreeUidListProvider
+     */
+    protected $treeUidListProvider;
+
+    /**
+     * @var ServerRequestHandler
+     */
+    protected $requestHandler;
+
+    /**
+     * @var UserService
+     */
+    protected $userService;
+
+    /**
+     * @var RedirectConfiguration
+     */
+    protected $configuration;
+
+    /**
+     * @var EventDispatcherInterface
+     */
+    protected $eventDispatcher;
+
+    public function __construct(
+        RedirectHandler $redirectHandler,
+        TreeUidListProvider $treeUidListProvider,
+        ServerRequestHandler $requestHandler,
+        UserService $userService,
+        EventDispatcherInterface $eventDispatcher
+    ) {
+        $this->redirectHandler = $redirectHandler;
+        $this->treeUidListProvider = $treeUidListProvider;
+        $this->requestHandler = $requestHandler;
+        $this->userService = $userService;
+        $this->eventDispatcher = $eventDispatcher;
+    }
+
+    /**
+     * Initialize redirects
+     */
+    public function initializeAction(): void
+    {
+        $this->loginType = (string)$this->requestHandler->getPropertyFromGetAndPost('logintype');
+
+        $this->configuration = new RedirectConfiguration(
+            (string)($this->settings['redirectMode'] ?? ''),
+            (string)($this->settings['redirectFirstMethod'] ?? ''),
+            (int)($this->settings['redirectPageLogin'] ?? 0),
+            (string)($this->settings['domains'] ?? ''),
+            (int)($this->settings['redirectPageLoginError'] ?? 0),
+            (int)($this->settings['redirectPageLogout'] ?? 0)
+        );
+
+        if ($this->isLoginOrLogoutInProgress() && !$this->isRedirectDisabled()) {
+            if ($this->userService->cookieWarningRequired()) {
+                $this->view->assign('cookieWarning', true);
+                return;
+            }
+
+            $redirectUrl = $this->redirectHandler->processRedirect(
+                $this->loginType,
+                $this->configuration,
+                $this->request->hasArgument('redirectReferrer') ? $this->request->getArgument('redirectReferrer') : ''
+            );
+            if ($redirectUrl !== '') {
+                $this->redirectToUri($redirectUrl);
+            }
+        }
+    }
+
+    /**
+     * Show login form
+     */
+    public function loginAction(): void
+    {
+        $this->handleLoginForwards();
+
+        $this->eventDispatcher->dispatch(new ModifyLoginFormViewEvent($this->view));
+
+        $this->view->assignMultiple(
+            [
+                'messageKey' => $this->getStatusMessageKey(),
+                'storagePid' => $this->getStoragePid(),
+                'permaloginStatus' => $this->getPermaloginStatus(),
+                'redirectURL' => $this->redirectHandler->getLoginFormRedirectUrl($this->configuration->getModes(), $this->configuration->getPageOnLogin(), $this->isRedirectDisabled()),
+                'redirectReferrer' => $this->request->hasArgument('redirectReferrer') ? (string)$this->request->getArgument('redirectReferrer'): '',
+                'referer' => $this->requestHandler->getPropertyFromGetAndPost('referer'),
+                'noRedirect' => $this->isRedirectDisabled(),
+            ]
+        );
+    }
+
+    /**
+     * User overview for logged in users
+     *
+     * @param bool $showLoginMessage
+     * @throws StopActionException
+     */
+    public function overviewAction(bool $showLoginMessage = false): void
+    {
+        if (!$this->userService->isUserLoggedIn()) {
+            $this->forward('login');
+        }
+
+        $this->eventDispatcher->dispatch(new LoginConfirmedEvent($this, $this->view));
+
+        $this->view->assignMultiple(
+            [
+                'user' => $this->userService->getFeUserData(),
+                'showLoginMessage' => $showLoginMessage,
+            ]
+        );
+    }
+
+    /**
+     * Show logout form
+     */
+    public function logoutAction(int $redirectPageLogout = 0): void
+    {
+        $this->view->assignMultiple(
+            [
+                'user' => $this->userService->getFeUserData(),
+                'storagePid' => $this->getStoragePid(),
+                'noRedirect' => $this->isRedirectDisabled(),
+                'actionUri' => $this->redirectHandler->getLogoutFormRedirectUrl($this->configuration->getModes(), $redirectPageLogout, $this->isRedirectDisabled()),
+            ]
+        );
+    }
+
+    /**
+     * Returns the parsed storagePid list including recursions
+     *
+     * @return string
+     */
+    protected function getStoragePid(): string
+    {
+        return $this->treeUidListProvider->getListForIdList(
+            (string)$this->settings['pages'],
+            (int)$this->settings['recursive']
+        );
+    }
+
+    /**
+     * Handle forwards to overview and logout actions from login action
+     */
+    protected function handleLoginForwards(): void
+    {
+        if ($this->shouldRedirectToOverview()) {
+            $this->forward('overview', null, null, ['showLoginMessage' => true]);
+        }
+
+        if ($this->userService->isUserLoggedIn()) {
+            $this->forward('logout');
+        }
+    }
+
+    /**
+     * The permanent login checkbox should only be shown if permalogin is not deactivated (-1),
+     * not forced to be always active (2) and lifetime is greater than 0
+     *
+     * @return int
+     */
+    protected function getPermaloginStatus(): int
+    {
+        $permaLogin = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
+
+        return $this->isPermaloginDisabled($permaLogin) ? -1 : $permaLogin;
+    }
+
+    protected function isPermaloginDisabled(int $permaLogin): bool
+    {
+        return $permaLogin > 1
+               || (int)($this->settings['showPermaLogin'] ?? 0) === 0
+               || $GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime'] === 0;
+    }
+
+    /**
+     * Redirect to overview on login successful and setting showLogoutFormAfterLogin disabled
+     *
+     * @return bool
+     */
+    protected function shouldRedirectToOverview(): bool
+    {
+        return $this->userService->isUserLoggedIn()
+               && ($this->loginType === LoginType::LOGIN)
+               && !($this->settings['showLogoutFormAfterLogin'] ?? 0);
+    }
+
+    /**
+     * Return message key based on user login status
+     *
+     * @return string
+     */
+    protected function getStatusMessageKey(): string
+    {
+        $messageKey = self::MESSAGEKEY_DEFAULT;
+        if ($this->loginType === LoginType::LOGIN && !$this->userService->isUserLoggedIn()) {
+            $messageKey = self::MESSAGEKEY_ERROR;
+        } elseif ($this->loginType === LoginType::LOGOUT) {
+            $messageKey = self::MESSAGEKEY_LOGOUT;
+        }
+
+        return $messageKey;
+    }
+
+    protected function isLoginOrLogoutInProgress(): bool
+    {
+        return $this->loginType === LoginType::LOGIN || $this->loginType === LoginType::LOGOUT;
+    }
+
+    /**
+     * Is redirect disabled by setting or noredirect parameter
+     *
+     * @return bool
+     */
+    public function isRedirectDisabled(): bool
+    {
+        return
+            $this->request->hasArgument('noredirect')
+            || ($this->settings['noredirect'] ?? false)
+            || ($this->settings['redirectDisable'] ?? false);
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Controller/PasswordRecoveryController.php b/typo3/sysext/felogin/Classes/Controller/PasswordRecoveryController.php
new file mode 100644 (file)
index 0000000..a3f3dcc
--- /dev/null
@@ -0,0 +1,299 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Domain\Model\FrontendUser;
+use TYPO3\CMS\Extbase\Error\Error;
+use TYPO3\CMS\Extbase\Error\Result;
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
+use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
+use TYPO3\CMS\Extbase\Mvc\Exception\UnsupportedRequestTypeException;
+use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
+use TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent;
+use TYPO3\CMS\FrontendLogin\Helper\TreeUidListProvider;
+use TYPO3\CMS\FrontendLogin\Service\RecoveryServiceInterface;
+use TYPO3\CMS\FrontendLogin\Service\ValidatorResolverService;
+
+/**
+ * Class PasswordRecoveryController
+ *
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class PasswordRecoveryController extends ActionController
+{
+    /**
+     * @var RecoveryServiceInterface
+     */
+    protected $recoveryService;
+
+    /**
+     * @var FrontendUserRepository
+     */
+    protected $userRepository;
+
+    /**
+     * @var EventDispatcherInterface
+     */
+    protected $eventDispatcher;
+
+    public function __construct(
+        EventDispatcherInterface $eventDispatcher,
+        RecoveryServiceInterface $recoveryService,
+        FrontendUserRepository $userRepository
+    ) {
+        $this->eventDispatcher = $eventDispatcher;
+        $this->recoveryService = $recoveryService;
+        $this->userRepository = $userRepository;
+    }
+
+    /**
+     * Shows the recovery form. If $userIdentifier is set an email will be sent, if the corresponding user exists
+     *
+     * @param string|null $userIdentifier
+     *
+     * @throws StopActionException
+     * @throws UnsupportedRequestTypeException
+     */
+    public function recoveryAction(string $userIdentifier = null): void
+    {
+        if (empty($userIdentifier)) {
+            return;
+        }
+
+        $storageProvider = $this->getTreeListUidProvider();
+        $storageList = $storageProvider->getListForIdList(
+            (string)$this->settings['pages'],
+            (int)$this->settings['recursive']
+        );
+
+        $email = $this->userRepository->findEmailByUsernameOrEmailOnPages(
+            $userIdentifier,
+            GeneralUtility::intExplode(',', $storageList, true)
+        );
+
+        if ($email) {
+            $this->recoveryService->sendRecoveryEmail($email);
+        }
+
+        $this->addFlashMessage($this->getTranslation('forgot_reset_message_emailSent'));
+
+        $this->redirect('login', 'Login', 'felogin');
+    }
+
+    /**
+     * Validate hash and make sure it's not expired. If it is not in the correct format or not set at all, a redirect
+     * to recoveryAction() is made, without further information.
+     *
+     * @throws AspectNotFoundException
+     * @throws NoSuchArgumentException
+     * @throws StopActionException
+     * @throws UnsupportedRequestTypeException
+     */
+    public function initializeShowChangePasswordAction(): void
+    {
+        $hash = $this->request->hasArgument('hash') ? $this->request->getArgument('hash') : '';
+
+        if (!$this->hasValidHash($hash)) {
+            $this->redirect('recovery', 'PasswordRecovery', 'felogin');
+        }
+
+        $timestamp = (int)GeneralUtility::trimExplode('|', $hash)[0];
+        $currentTimestamp = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('date', 'timestamp');
+
+        // timestamp is expired or hash can not be assigned to a user
+        if ($currentTimestamp > $timestamp || !$this->userRepository->existsUserWithHash($hash)) {
+            $result = $this->request->getOriginalRequestMappingResults();
+            $result->addError(new Error($this->getTranslation('change_password_notvalid_message'), 1554994253));
+            $this->request->setOriginalRequestMappingResults($result);
+            $this->forward('recovery', 'PasswordRecovery', 'felogin');
+        }
+    }
+
+    /**
+     * Show the change password form but only if a hash exists (from get parameters).
+     *
+     * @param string $hash
+     */
+    public function showChangePasswordAction(string $hash): void
+    {
+        $this->view->assign('hash', $hash);
+    }
+
+    /**
+     * Validate entered password and passwordRepeat values. If they are invalid a forward() to
+     * showChangePasswordAction() takes place. All validation errors are put into the request mapping results.
+     *
+     * Used validators are configured via TypoScript settings.
+     *
+     * @throws NoSuchArgumentException
+     * @throws StopActionException
+     */
+    public function initializeChangePasswordAction(): void
+    {
+        // Exit early if newPass or newPassRepeat is not set.
+        $originalResult = $this->request->getOriginalRequestMappingResults();
+        $argumentsExist = $this->request->hasArgument('newPass') && $this->request->hasArgument('newPassRepeat');
+        $argumentsEmpty = empty($this->request->getArgument('newPass')) || empty($this->request->getArgument('newPassRepeat'));
+
+        if (!$argumentsExist || $argumentsEmpty) {
+            $originalResult->addError(new Error(
+                $this->getTranslation('empty_password_and_password_repeat'),
+                1554971665
+            ));
+            $this->request->setOriginalRequestMappingResults($originalResult);
+            $this->forward(
+                'showChangePassword',
+                'PasswordRecovery',
+                'felogin',
+                ['hash' => $this->request->getArgument('hash')]
+            );
+        }
+
+        $this->validateNewPassword($originalResult);
+
+        // todo: check if calling $this->errorAction is necessary here
+        // if an error exists, forward with all messages to the change password form
+        if ($originalResult->hasErrors()) {
+            $this->forward(
+                'showChangePassword',
+                'PasswordRecovery',
+                'felogin',
+                ['hash' => $this->request->getArgument('hash')]
+            );
+        }
+    }
+
+    /**
+     * Change actual password. Hash $newPass and update the user with the corresponding $hash.
+     *
+     * @param string $newPass
+     * @param string $hash
+     *
+     * @throws InvalidPasswordHashException
+     * @throws StopActionException
+     * @throws UnsupportedRequestTypeException
+     * @throws AspectNotFoundException
+     */
+    public function changePasswordAction(string $newPass, string $hash): void
+    {
+        $passwordHash = GeneralUtility::makeInstance(PasswordHashFactory::class)
+            ->getDefaultHashInstance('FE')
+            ->getHashedPassword($newPass);
+
+        $passwordHash = $this->notifyPasswordChange($newPass, $passwordHash, $hash);
+        $this->userRepository->updatePasswordAndInvalidateHash($hash, $passwordHash);
+
+        $this->addFlashMessage($this->getTranslation('change_password_done_message'));
+
+        $this->redirect('login', 'Login', 'felogin');
+    }
+
+    /**
+     * @param Result $originalResult
+     *
+     * @throws NoSuchArgumentException
+     */
+    protected function validateNewPassword(Result $originalResult): void
+    {
+        $newPass = $this->request->getArgument('newPass');
+
+        // make sure the user entered the password twice
+        if ($newPass !== $this->request->getArgument('newPassRepeat')) {
+            $originalResult->addError(new Error($this->getTranslation('password_must_match_repeated'), 1554912163));
+        }
+
+        // Resolve validators from TypoScript configuration
+        $validators = GeneralUtility::makeInstance(ValidatorResolverService::class)
+            ->resolve($this->settings['passwordValidators']);
+
+        // Call each validator on new password
+        foreach ($validators ?? [] as $validator) {
+            $result = $validator->validate($newPass);
+            $originalResult->merge($result);
+        }
+
+        //set the result from all validators
+        $this->request->setOriginalRequestMappingResults($originalResult);
+    }
+
+    /**
+     * Wrapper to mock LocalizationUtility::translate
+     *
+     * @param string $key
+     *
+     * @return string
+     */
+    protected function getTranslation(string $key): string
+    {
+        return (string)LocalizationUtility::translate($key, 'felogin');
+    }
+
+    /**
+     * Validates that $hash is in the expected format (timestamp|forgot_hash)
+     *
+     * @param string $hash
+     *
+     * @return bool
+     */
+    protected function hasValidHash($hash): bool
+    {
+        return !empty($hash) && is_string($hash) && strpos($hash, '|') === 10;
+    }
+
+    protected function getTreeListUidProvider(): TreeUidListProvider
+    {
+        return GeneralUtility::makeInstance(
+            TreeUidListProvider::class,
+            $this->configurationManager->getContentObject()
+        );
+    }
+
+    /**
+     * @param string $newPassword Unencrypted new password
+     * @param string $hashedPassword New password hash passed as reference
+     * @param string $hash Forgot password hash
+     * @return string
+     * @throws StopActionException
+     */
+    protected function notifyPasswordChange(string $newPassword, string $hashedPassword, string $hash): string
+    {
+        /** @var FrontendUser $user */
+        $user = $this->userRepository->findOneByForgotPasswordHash($hash);
+        $event = new PasswordChangeEvent($user, $hashedPassword, $newPassword);
+        if ($event->isPropagationStopped()) {
+            $requestResult = $this->request->getOriginalRequestMappingResults();
+            $requestResult->addError(new Error($event->getErrorMessage(), 1562846833));
+            $this->request->setOriginalRequestMappingResults($requestResult);
+
+            $this->forward(
+                'showChangePassword',
+                'PasswordRecovery',
+                'felogin',
+                ['hash' => $hash]
+            );
+        }
+        return $hashedPassword;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserGroupRepository.php b/typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserGroupRepository.php
new file mode 100644 (file)
index 0000000..11609a5
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Domain\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class FrontendUserGroupRepository
+{
+    /**
+     * @var Connection
+     */
+    private $connection;
+
+    /**
+     * @var string
+     */
+    private $table;
+
+    /**
+     * @param UserService $userService
+     * @param ConnectionPool $connectionPool
+     */
+    public function __construct(UserService $userService, ConnectionPool $connectionPool)
+    {
+        $this->table = $userService->getFeUserGroupTable();
+        $this->connection = $connectionPool->getConnectionForTable($this->getTable());
+    }
+
+    public function getTable(): string
+    {
+        return $this->table;
+    }
+
+    /**
+     * @param int $groupId
+     * @return int|null
+     */
+    public function findRedirectPageIdByGroupId(int $groupId): ?int
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+        $queryBuilder->getRestrictions()->removeAll();
+
+        $query = $queryBuilder
+            ->select('felogin_redirectPid')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->neq(
+                    'felogin_redirectPid',
+                    $this->connection->quote('')
+                ),
+                $queryBuilder->expr()->eq(
+                    'uid',
+                    $queryBuilder->createNamedParameter($groupId, Connection::PARAM_INT)
+                )
+            )
+            ->setMaxResults(1)
+        ;
+
+        $column = $query->execute()->fetchColumn();
+        return $column === false ? null : (int)$column;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserRepository.php b/typo3/sysext/felogin/Classes/Domain/Repository/FrontendUserRepository.php
new file mode 100644 (file)
index 0000000..762a99f
--- /dev/null
@@ -0,0 +1,244 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Domain\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Doctrine\DBAL\FetchMode;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class FrontendUserRepository
+{
+    /**
+     * @var Connection
+     */
+    private $connection;
+
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var UserService
+     */
+    private $userService;
+
+    /**
+     * @param ConnectionPool $connectionPool
+     * @param Context $context
+     */
+    public function __construct(
+        UserService $userService,
+        ConnectionPool $connectionPool,
+        Context $context
+    ) {
+        $this->userService = $userService;
+        $this->connection = $connectionPool->getConnectionForTable($this->getTable());
+        $this->context = $context;
+    }
+
+    public function getTable(): string
+    {
+        return $this->userService->getFeUserTable();
+    }
+
+    /**
+     * Change the password for a user based on forgot password hash.
+     *
+     * @param string $forgotPasswordHash The hash of the feUser that should be resolved.
+     * @param string $password The new password.
+     * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
+     */
+    public function updatePasswordAndInvalidateHash(string $forgotPasswordHash, string $password): void
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+
+        $currentTimestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
+        $query = $queryBuilder
+            ->update($this->getTable())
+            ->set('password', $password)
+            ->set('felogin_forgotHash', $this->connection->quote(''), false)
+            ->set('tstamp', $currentTimestamp)
+            ->where(
+                $queryBuilder->expr()->eq('felogin_forgotHash', $queryBuilder->createNamedParameter($forgotPasswordHash))
+            )
+        ;
+        $query->execute();
+    }
+
+    /**
+     * Returns true if an user exists with hash as `felogin_forgothash`, otherwise false.
+     *
+     * @param string $hash The hash of the feUser that should be check for existence.
+     * @return bool Either true or false based on the existence of the user.
+     */
+    public function existsUserWithHash(string $hash): bool
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+
+        $query = $queryBuilder
+            ->count('uid')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'felogin_forgotHash',
+                    $queryBuilder->createNamedParameter($hash)
+                )
+            )
+        ;
+
+        return (bool)$query->execute()->fetchColumn();
+    }
+
+    /**
+     * Sets forgot hash for passed email address.
+     *
+     * @param string $emailAddress
+     * @param string $hash
+     */
+    public function updateForgotHashForUserByEmail(string $emailAddress, string $hash): void
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+        $query = $queryBuilder
+            ->update($this->getTable())
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'email',
+                    $queryBuilder->createNamedParameter($emailAddress)
+                )
+            )
+            ->set('felogin_forgotHash', $hash)
+        ;
+        $query->execute();
+    }
+
+    /**
+     * Fetches array containing uid, username, email, first_name, middle_name & last_name by email.
+     *
+     * @param string $emailAddress
+     * @return array
+     */
+    public function fetchUserInformationByEmail(string $emailAddress): array
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+
+        $query = $queryBuilder
+            ->select('uid', 'username', 'email', 'first_name', 'middle_name', 'last_name')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'email',
+                    $queryBuilder->createNamedParameter($emailAddress)
+                )
+            )
+        ;
+
+        return $query->execute()->fetch(FetchMode::ASSOCIATIVE);
+    }
+
+    /**
+     * @param string $usernameOrEmail
+     * @param array $pages
+     * @return string|null
+     */
+    public function findEmailByUsernameOrEmailOnPages(string $usernameOrEmail, array $pages = []): ?string
+    {
+        if ($usernameOrEmail === '') {
+            return null;
+        }
+
+        $queryBuilder = $this->connection->createQueryBuilder();
+        $query = $queryBuilder
+            ->select('email')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->orX(
+                    $queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($usernameOrEmail)),
+                    $queryBuilder->expr()->eq('email', $queryBuilder->createNamedParameter($usernameOrEmail))
+                )
+            )
+        ;
+
+        if (!empty($pages)) {
+            // respect storage pid
+            $query->andWhere($queryBuilder->expr()->in('pid', $pages));
+        }
+
+        $column = $query->execute()->fetchColumn();
+        return $column === false || $column === '' ? null : (string)$column;
+    }
+
+    /**
+     * @param string $hash
+     * @return array|null
+     */
+    public function findOneByForgotPasswordHash(string $hash): ?array
+    {
+        if ($hash === '') {
+            return null;
+        }
+
+        $queryBuilder = $this->connection->createQueryBuilder();
+        $query = $queryBuilder
+            ->select('*')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'felogin_forgotHash',
+                    $queryBuilder->createNamedParameter($hash)
+                )
+            )
+            ->setMaxResults(1)
+        ;
+
+        $row = $query->execute()->fetch();
+        return is_array($row) ? $row : null;
+    }
+
+    /**
+     * @param int $uid
+     * @return int|null
+     */
+    public function findRedirectIdPageByUserId(int $uid): ?int
+    {
+        $queryBuilder = $this->connection->createQueryBuilder();
+        $queryBuilder->getRestrictions()->removeAll();
+        $query = $queryBuilder
+            ->select('felogin_redirectPid')
+            ->from($this->getTable())
+            ->where(
+                $queryBuilder->expr()->neq(
+                    'felogin_redirectPid',
+                    $this->connection->quote('')
+                ),
+                $queryBuilder->expr()->eq(
+                    $this->userService->getFeUserIdColumn(),
+                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
+                )
+            )
+            ->setMaxResults(1)
+        ;
+
+        $column = $query->execute()->fetchColumn();
+        return $column === false ? null : (int)$column;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Event/LoginConfirmedEvent.php b/typo3/sysext/felogin/Classes/Event/LoginConfirmedEvent.php
new file mode 100644 (file)
index 0000000..babfbb4
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\FrontendLogin\Event;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
+use TYPO3\CMS\FrontendLogin\Controller\LoginController;
+
+/**
+ * A notification when a log in has successfully arrived at the plugin, via the view and the controller, multiple
+ * information can be overridden in Event Listeners.
+ */
+final class LoginConfirmedEvent
+{
+    /**
+     * @var LoginController
+     */
+    private $controller;
+
+    /**
+     * @var ViewInterface
+     */
+    private $view;
+
+    public function __construct(LoginController $controller, ViewInterface $view)
+    {
+        $this->controller = $controller;
+        $this->view = $view;
+    }
+
+    public function getController(): LoginController
+    {
+        return $this->controller;
+    }
+
+    public function getView(): ViewInterface
+    {
+        return $this->view;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Event/ModifyLoginFormViewEvent.php b/typo3/sysext/felogin/Classes/Event/ModifyLoginFormViewEvent.php
new file mode 100644 (file)
index 0000000..76aefe7
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\FrontendLogin\Event;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
+
+/**
+ * Allows to inject custom variables into the login form.
+ */
+final class ModifyLoginFormViewEvent
+{
+    /**
+     * @var ViewInterface
+     */
+    private $view;
+
+    public function __construct(ViewInterface $view)
+    {
+        $this->view = $view;
+    }
+
+    public function getView(): ViewInterface
+    {
+        return $this->view;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Event/PasswordChangeEvent.php b/typo3/sysext/felogin/Classes/Event/PasswordChangeEvent.php
new file mode 100644 (file)
index 0000000..568b58f
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\FrontendLogin\Event;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\EventDispatcher\StoppableEventInterface;
+use TYPO3\CMS\Extbase\Domain\Model\FrontendUser;
+
+/**
+ * Event that contains information about the password which was set, and is about to be stored in the database.
+ *
+ * Additional validation can happen here.
+ */
+final class PasswordChangeEvent implements StoppableEventInterface
+{
+    /**
+     * @var bool
+     */
+    private $invalid = false;
+
+    /**
+     * @var string
+     */
+    private $errorMessage;
+
+    /**
+     * @var FrontendUser
+     */
+    private $user;
+
+    /**
+     * @var string
+     */
+    private $passwordHash;
+
+    /**
+     * @var string
+     */
+    private $rawPassword;
+
+    public function __construct(FrontendUser $user, string $newPasswordHash, string $rawNewPassword)
+    {
+        $this->user = $user;
+        $this->passwordHash = $newPasswordHash;
+        $this->rawPassword = $rawNewPassword;
+    }
+
+    public function getUser(): FrontendUser
+    {
+        return $this->user;
+    }
+
+    public function getHashedPassword(): string
+    {
+        return $this->passwordHash;
+    }
+
+    public function setHashedPassword(string $passwordHash): void
+    {
+        $this->passwordHash = $passwordHash;
+    }
+
+    public function getRawPassword(): string
+    {
+        return $this->rawPassword;
+    }
+
+    public function setAsInvalid(string $message)
+    {
+        $this->invalid = true;
+        $this->errorMessage = $message;
+    }
+
+    public function getErrorMessage(): ?string
+    {
+        return $this->errorMessage;
+    }
+
+    public function isPropagationStopped(): bool
+    {
+        return $this->invalid;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Event/SendRecoveryEmailEvent.php b/typo3/sysext/felogin/Classes/Event/SendRecoveryEmailEvent.php
new file mode 100644 (file)
index 0000000..19e73df
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\FrontendLogin\Event;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\Mime\Email;
+
+/**
+ * Event that contains the email to be sent to the user when they request a new password.
+ * More
+ *
+ * Additional validation can happen here.
+ */
+final class SendRecoveryEmailEvent
+{
+    /**
+     * @var Email
+     */
+    private $email;
+
+    /**
+     * @var array
+     */
+    private $user;
+
+    public function __construct(Email $email, array $user)
+    {
+        $this->email = $email;
+        $this->user = $user;
+    }
+
+    public function getUserInformation(): array
+    {
+        return $this->user;
+    }
+
+    public function getEmail(): Email
+    {
+        return $this->email;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Helper/TreeUidListProvider.php b/typo3/sysext/felogin/Classes/Helper/TreeUidListProvider.php
new file mode 100644 (file)
index 0000000..e6a3ae6
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Helper;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+final class TreeUidListProvider
+{
+    /**
+     * @var ContentObjectRenderer
+     */
+    protected $cObj;
+
+    /**
+     * TreeUidListProvider constructor.
+     * @param ContentObjectRenderer $cObj
+     */
+    public function __construct(ContentObjectRenderer $cObj)
+    {
+        $this->cObj = $cObj;
+    }
+
+    /**
+     * Fetches uid list of pages beneath passed list of page-ids and returns them as a comma separated string.
+     *
+     * @param string $uidList Comma separated list of ids.
+     * @param int $depth The number of levels to descend. If you want to descend infinitely, just set this to 100 or so. Should be at least "1" since zero will just make the function return (no decend...)
+     * @param bool $uniqueIds Removes duplicated ids in returned string if set to true.
+     * @return string Comma separated uid list
+     */
+    public function getListForIdList(string $uidList, int $depth = 0, bool $uniqueIds = true): string
+    {
+        if ($depth === 0) {
+            return $uidList;
+        }
+
+        $list = GeneralUtility::trimExplode(',', $uidList);
+
+        foreach ($list as $uid) {
+            $pidList[] = $this->cObj->getTreeList($uid, $depth);
+        }
+
+        $uidTreeList = implode(',', $pidList ?? []);
+
+        if ($uniqueIds) {
+            $uidTreeList = $this->removeDuplicatedIds($uidTreeList);
+        }
+
+        return $uidTreeList;
+    }
+
+    /**
+     * Removes duplicated ids from comma separated uid list
+     *
+     * @param string $uidList
+     * @return string
+     */
+    protected function removeDuplicatedIds(string $uidList): string
+    {
+        $uniqueUidArray = array_unique(GeneralUtility::trimExplode(',', $uidList));
+
+        return implode(',', $uniqueUidArray);
+    }
+}
index fc21b6c..1ca310a 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-namespace TYPO3\CMS\Felogin\Hooks;
+namespace TYPO3\CMS\FrontendLogin\Hooks;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -14,34 +14,42 @@ namespace TYPO3\CMS\Felogin\Hooks;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\View\PageLayoutView;
+use TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
 use TYPO3\CMS\Core\Localization\LanguageService;
 
 /**
- * Hook to display verbose information about the felogin plugin
+ * Hook to display verbose information about the felogin plugin in the page module
+ *
  * @internal this is a TYPO3 hook implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
  */
-class CmsLayout implements \TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface
+final class CmsLayout implements PageLayoutViewDrawItemHookInterface
 {
     /**
      * Preprocesses the preview rendering of a content element.
      *
-     * @param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject Calling parent object
+     * @param PageLayoutView $parentObject Calling parent object
      * @param bool $drawItem Whether to draw the item using the default functionalities
      * @param string $headerContent Header content
      * @param string $itemContent Item content
      * @param array $row Record row of tt_content
      */
-    public function preProcess(\TYPO3\CMS\Backend\View\PageLayoutView &$parentObject, &$drawItem, &$headerContent, &$itemContent, array &$row)
+    public function preProcess(PageLayoutView &$parentObject, &$drawItem, &$headerContent, &$itemContent, array &$row)
     {
-        if ($row['CType'] === 'login') {
-            $drawItem = false;
-            $itemContent .= $parentObject->linkEditContent('<strong>' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_title')) . '</strong>', $row);
+        if ($row['CType'] !== 'login') {
+            return;
         }
+        $drawItem = false;
+        $itemContent .= $parentObject->linkEditContent(
+            '<strong>' . htmlspecialchars(
+                $this->getLanguageService()->sL(
+                    'LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_title'
+                )
+            ) . '</strong>',
+            $row
+        );
     }
 
-    /**
-     * @return LanguageService|null
-     */
     protected function getLanguageService(): ?LanguageService
     {
         return $GLOBALS['LANG'] ?? null;
diff --git a/typo3/sysext/felogin/Classes/Redirect/RedirectHandler.php b/typo3/sysext/felogin/Classes/Redirect/RedirectHandler.php
new file mode 100644 (file)
index 0000000..bdb8fd8
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Redirect;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Authentication\LoginType;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration;
+
+/**
+ * Do felogin related redirects
+ *
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class RedirectHandler
+{
+    /**
+     * @var bool
+     */
+    protected $userIsLoggedIn = false;
+
+    /**
+     * @var ServerRequestHandler
+     */
+    protected $requestHandler;
+
+    /**
+     * @var RedirectModeHandler
+     */
+    protected $redirectModeHandler;
+
+    /**
+     * @var Context
+     */
+    protected $context;
+
+    public function __construct(
+        ServerRequestHandler $requestHandler,
+        RedirectModeHandler $redirectModeHandler,
+        Context $context
+    ) {
+        $this->requestHandler = $requestHandler;
+        $this->redirectModeHandler = $redirectModeHandler;
+        $this->context = $context;
+        $this->userIsLoggedIn = (bool)$this->context->getPropertyFromAspect('frontend.user', 'isLoggedIn');
+    }
+
+    /**
+     * Process redirect modes. The function searches for a redirect url using all configured modes.
+     *
+     * @param $loginType
+     * @param \TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration $configuration
+     * @param string $redirectModeReferrer
+     * @return string Redirect URL
+     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
+     */
+    public function processRedirect($loginType, RedirectConfiguration $configuration, string $redirectModeReferrer = ''): string
+    {
+        if ($this->isUserLoginFailedAndLoginErrorActive($configuration->getModes(), $loginType)) {
+            return $this->redirectModeHandler->redirectModeLoginError($configuration->getPageOnLoginError());
+        }
+
+        $redirectUrlList = [];
+        foreach ($configuration->getModes() as $redirectMode) {
+            $redirectUrl = '';
+
+            if ($loginType === LoginType::LOGIN) {
+                $redirectUrl = $this->handleSuccessfulLogin($redirectMode, $configuration->getPageOnLogin(), $configuration->getDomains(), $redirectModeReferrer);
+            } elseif ($loginType === LoginType::LOGOUT) {
+                $redirectUrl = $this->handleSuccessfulLogout($redirectMode, $configuration->getPageOnLogout());
+            }
+
+            if ($redirectUrl !== '') {
+                $redirectUrlList[] = $redirectUrl;
+            }
+        }
+
+        return $this->fetchReturnUrlFromList($redirectUrlList, $configuration->getFirstMode());
+    }
+
+    /**
+     * Get alternative logout form redirect url if logout and page not accessible
+     *
+     * @param array $redirectModes
+     * @param int $redirectPageLogout
+     * @return string
+     */
+    public function getLogoutRedirectUrl(array $redirectModes, int $redirectPageLogout = 0): string
+    {
+        if ($this->userIsLoggedIn && $this->isRedirectModeActive($redirectModes, RedirectMode::LOGOUT)) {
+            $redirectUrl = $this->redirectModeHandler->redirectModeLogout($redirectPageLogout);
+        } else {
+            $redirectUrl = $this->getGetpostRedirectUrl($redirectModes);
+        }
+
+        return $redirectUrl;
+    }
+
+    /**
+     * Get alternative login form redirect url
+     *
+     * @param array $redirectModes
+     * @param int $redirectPageLogin
+     * @return string
+     */
+    protected function getLoginRedirectUrl(array $redirectModes, int $redirectPageLogin): string
+    {
+        if ($this->isRedirectModeActive($redirectModes, RedirectMode::LOGIN)) {
+            $redirectUrl = $this->redirectModeHandler->redirectModeLogin($redirectPageLogin);
+        } else {
+            $redirectUrl = $this->getGetpostRedirectUrl($redirectModes);
+        }
+
+        return $redirectUrl;
+    }
+
+    /**
+     * Is used for alternative redirect urls on redirect mode "getpost"
+     *
+     * @param array $redirectModes
+     * @return string
+     */
+    protected function getGetpostRedirectUrl(array $redirectModes): string
+    {
+        return $this->isRedirectModeActive($redirectModes, RedirectMode::GETPOST)
+            ? $this->requestHandler->getRedirectUrlRequestParam()
+            : '';
+    }
+
+    /**
+     * Handle redirect mode logout
+     *
+     * @param string $redirectMode
+     * @param int $redirectPageLogout
+     * @return string
+     */
+    protected function handleSuccessfulLogout(string $redirectMode, int $redirectPageLogout): string
+    {
+        $redirectUrl = '';
+        if ($redirectMode === RedirectMode::LOGOUT) {
+            $redirectUrl = $this->redirectModeHandler->redirectModeLogout($redirectPageLogout);
+        }
+
+        return $redirectUrl;
+    }
+
+    /**
+     * Base on setting redirectFirstMethod get first or last entry from redirect url list.
+     *
+     * @param array $redirectUrlList
+     * @param $redirectFirstMethod
+     * @return string
+     */
+    protected function fetchReturnUrlFromList(array $redirectUrlList, $redirectFirstMethod): string
+    {
+        if (count($redirectUrlList) === 0) {
+            return '';
+        }
+
+        // Remove empty values, but keep "0" as value (that's why "strlen" is used as second parameter)
+        $redirectUrlList = array_filter($redirectUrlList, 'strlen');
+
+        return $redirectFirstMethod
+            ? array_shift($redirectUrlList)
+            : array_pop($redirectUrlList);
+    }
+
+    /**
+     * Generate redirect_url for case that the user was successfuly logged in
+     *
+     * @param string $redirectMode
+     * @param int $redirectPageLogin
+     * @param string $domains
+     * @param string $redirectModeReferrer
+     * @return string
+     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException
+     */
+    protected function handleSuccessfulLogin(string $redirectMode, int $redirectPageLogin = 0, string $domains = '', string $redirectModeReferrer = ''): string
+    {
+        if (!$this->userIsLoggedIn) {
+            return '';
+        }
+
+        // Logintype is needed because the login-page wouldn't be accessible anymore after a login (would always redirect)
+        switch ($redirectMode) {
+            case RedirectMode::GROUP_LOGIN:
+                $redirectUrl = $this->redirectModeHandler->redirectModeGroupLogin();
+                break;
+            case RedirectMode::USER_LOGIN:
+                $redirectUrl = $this->redirectModeHandler->redirectModeUserLogin();
+                break;
+            case RedirectMode::LOGIN:
+                $redirectUrl = $this->redirectModeHandler->redirectModeLogin($redirectPageLogin);
+                break;
+            case RedirectMode::GETPOST:
+                $redirectUrl = $this->requestHandler->getRedirectUrlRequestParam();
+                break;
+            case RedirectMode::REFERER:
+                $redirectUrl = $this->redirectModeHandler->redirectModeReferrer($redirectModeReferrer);
+                break;
+            case RedirectMode::REFERER_DOMAINS:
+                $redirectUrl = $this->redirectModeHandler->redirectModeRefererDomains($domains, $redirectModeReferrer);
+                break;
+            default:
+                $redirectUrl = '';
+        }
+
+        return $redirectUrl;
+    }
+
+    protected function isUserLoginFailedAndLoginErrorActive(array $redirectModes, string $loginType): bool
+    {
+        return $loginType === LoginType::LOGIN
+            && $this->userIsLoggedIn === false
+            && $this->isRedirectModeActive($redirectModes, RedirectMode::LOGIN_ERROR);
+    }
+
+    /**
+     * Checks if the give mode is active or not
+     *
+     * @param string $mode
+     * @param array $redirectModes
+     * @return bool
+     */
+    public function isRedirectModeActive(array $redirectModes, string $mode): bool
+    {
+        return in_array($mode, $redirectModes, true);
+    }
+
+    /**
+     * Returns the redirect Url that should be used in login form
+     *
+     * @param bool $redirectDisabled
+     * @param array $redirectModes
+     * @param int $redirectPageLogin
+     * @return string
+     */
+    public function getLoginFormRedirectUrl(array $redirectModes, int $redirectPageLogin, bool $redirectDisabled = false): string
+    {
+        if (!$redirectDisabled) {
+            $redirectUrl = $this->getLoginRedirectUrl($redirectModes, $redirectPageLogin);
+        } else {
+            $redirectUrl = $this->requestHandler->getRedirectUrlRequestParam();
+        }
+
+        return $redirectUrl;
+    }
+
+    /**
+     * Returns the redirect Url that should be used in logout form
+     *
+     * @param array $redirectModes
+     * @param int $redirectPageLogout
+     * @param bool $redirectDisabled
+     * @return string
+     */
+    public function getLogoutFormRedirectUrl(array $redirectModes, int $redirectPageLogout = 0, bool $redirectDisabled = false): string
+    {
+        if (!$redirectDisabled) {
+            $redirectUrl = $this->getLogoutRedirectUrl($redirectModes, $redirectPageLogout);
+        } else {
+            $redirectUrl = $this->requestHandler->getRedirectUrlRequestParam();
+        }
+
+        return $redirectUrl;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Redirect/RedirectMode.php b/typo3/sysext/felogin/Classes/Redirect/RedirectMode.php
new file mode 100644 (file)
index 0000000..67babb9
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Redirect;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Contains the different redirect modes types
+ *
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+final class RedirectMode
+{
+    public const LOGIN = 'login';
+    public const LOGOUT = 'logout';
+    public const LOGIN_ERROR = 'loginError';
+    public const GETPOST = 'getpost';
+    public const USER_LOGIN = 'userLogin';
+    public const GROUP_LOGIN = 'groupLogin';
+    public const REFERER = 'referer';
+    public const REFERER_DOMAINS = 'refererDomains';
+}
diff --git a/typo3/sysext/felogin/Classes/Redirect/RedirectModeHandler.php b/typo3/sysext/felogin/Classes/Redirect/RedirectModeHandler.php
new file mode 100644 (file)
index 0000000..a2d6aad
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Redirect;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
+use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserGroupRepository;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+use TYPO3\CMS\FrontendLogin\Validation\RedirectUrlValidator;
+
+/**
+ * Do felogin related redirects
+ *
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class RedirectModeHandler
+{
+    /**
+     * @var RedirectUrlValidator
+     */
+    protected $redirectUrlValidator;
+
+    /**
+     * @var UriBuilder
+     */
+    protected $uriBuilder;
+
+    /**
+     * @var ServerRequestHandler
+     */
+    protected $serverRequestHandler;
+
+    /**
+     * @var UserService
+     */
+    private $userService;
+
+    /**
+     * @var FrontendUserRepository
+     */
+    private $frontendUserRepository;
+
+    /**
+     * @var FrontendUserGroupRepository
+     */
+    private $frontendUserGroupRepository;
+
+    public function __construct(
+        UriBuilder $uriBuilder,
+        ServerRequestHandler $serverRequestHandler,
+        UserService $userService,
+        FrontendUserRepository $frontendUserRepository,
+        FrontendUserGroupRepository $frontendUserGroupRepository
+    ) {
+        $this->uriBuilder = $uriBuilder;
+        $this->redirectUrlValidator = GeneralUtility::makeInstance(
+            RedirectUrlValidator::class,
+            GeneralUtility::makeInstance(SiteFinder::class),
+            (int)$GLOBALS['TSFE']->id
+        );
+        $this->serverRequestHandler = $serverRequestHandler;
+        $this->userService = $userService;
+        $this->frontendUserRepository = $frontendUserRepository;
+        $this->frontendUserGroupRepository = $frontendUserGroupRepository;
+    }
+
+    /**
+     * Handle redirect mode groupLogin
+     *
+     * @return string
+     */
+    public function redirectModeGroupLogin(): string
+    {
+        // taken from dkd_redirect_at_login written by Ingmar Schlecht; database-field changed
+        $groupData = $this->userService->getFeUserGroupData();
+
+        $groupUids = array_unique(array_map('intval', $groupData['uid']));
+        if (empty($groupData['uid'])) {
+            return '';
+        }
+
+        // take the first group with a redirect page
+        $redirectPageId = $this->frontendUserGroupRepository->findRedirectPageIdByGroupId(
+            array_shift($groupUids)
+        );
+
+        if ($redirectPageId === null) {
+            return '';
+        }
+
+        return $this->buildUriForPageUid($redirectPageId);
+    }
+
+    /**
+     * Handle redirect mode userLogin
+     *
+     * @return string
+     */
+    public function redirectModeUserLogin(): string
+    {
+        $redirectPageId = $this->frontendUserRepository->findRedirectIdPageByUserId(
+            $this->userService->getFeUserData()['uid']
+        );
+
+        if ($redirectPageId === null) {
+            return '';
+        }
+
+        return $this->buildUriForPageUid($redirectPageId);
+    }
+
+    /**
+     * Handle redirect mode login
+     *
+     * @return string
+     */
+    public function redirectModeLogin(int $redirectPageLogin): string
+    {
+        if ($redirectPageLogin !== 0) {
+            $redirectUrl = $this->buildUriForPageUid($redirectPageLogin);
+        }
+
+        return $redirectUrl ?? '';
+    }
+
+    /**
+     * Handle redirect mode referrer
+     *
+     * @return string
+     * @throws NoSuchArgumentException
+     */
+    public function redirectModeReferrer(string $redirectReferrer): string
+    {
+        if ($redirectReferrer !== 'off') {
+            // Avoid forced logout, when trying to login immediately after a logout
+            $redirectUrl = preg_replace('/[&?]logintype=[a-z]+/', '', $this->getRefererRequestParam());
+        }
+
+        return $redirectUrl ?? '';
+    }
+
+    /**
+     * Handle redirect mode refererDomains
+     *
+     * @return string
+     * @throws NoSuchArgumentException
+     */
+    public function redirectModeRefererDomains(string $domains, string $redirectReferrer): string
+    {
+        if ($redirectReferrer !== '') {
+            return '';
+        }
+
+        // Auto redirect.
+        // Feature to redirect to the page where the user came from (HTTP_REFERER).
+        // Allowed domains to redirect to, can be configured with plugin.tx_felogin_login.domains
+        // Thanks to plan2.net / Martin Kutschker for implementing this feature.
+        // also avoid redirect when logging in after changing password
+        if ($domains) {
+            $url = $this->getRefererRequestParam();
+            // Is referring url allowed to redirect?
+            $match = [];
+            if (preg_match('#^http://([[:alnum:]._-]+)/#', $url, $match)) {
+                $redirectDomain = $match[1];
+                $found = false;
+                foreach (GeneralUtility::trimExplode(',', $domains, true) as $domain) {
+                    if (preg_match('/(?:^|\\.)' . $domain . '$/', $redirectDomain)) {
+                        $found = true;
+                        break;
+                    }
+                }
+                if (!$found) {
+                    $url = '';
+                }
+            }
+            // Avoid forced logout, when trying to login immediately after a logout
+            if ($url) {
+                $redirectUrl = preg_replace('/[&?]logintype=[a-z]+/', '', $url);
+            }
+        }
+
+        return $redirectUrl ?? '';
+    }
+
+    /**
+     * Handle redirect mode loginError after login-error
+     *
+     * @return string
+     */
+    public function redirectModeLoginError(int $redirectPageLoginError = 0): string
+    {
+        if ($redirectPageLoginError > 0) {
+            $redirectUrl = $this->buildUriForPageUid($redirectPageLoginError);
+        }
+
+        return $redirectUrl ?? '';
+    }
+
+    /**
+     * Handle redirect mode logout
+     *
+     * @param int $redirectPageLogout
+     * @return string
+     */
+    public function redirectModeLogout(int $redirectPageLogout): string
+    {
+        if ($redirectPageLogout > 0) {
+            $redirectUrl = $this->buildUriForPageUid($redirectPageLogout);
+        }
+
+        return $redirectUrl ?? '';
+    }
+
+    protected function buildUriForPageUid(int $pageUid): string
+    {
+        $this->uriBuilder->reset();
+        $this->uriBuilder->setTargetPageUid($pageUid);
+
+        return $this->uriBuilder->build();
+    }
+
+    protected function getRefererRequestParam(): string
+    {
+        $requestReferer = (string)$this->serverRequestHandler->getPropertyFromGetAndPost('referer');
+        if ($this->redirectUrlValidator->isValid($requestReferer)) {
+            $referer = $requestReferer;
+        }
+
+        return $referer ?? '';
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Redirect/ServerRequestHandler.php b/typo3/sysext/felogin/Classes/Redirect/ServerRequestHandler.php
new file mode 100644 (file)
index 0000000..77fe2b6
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Redirect;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\FrontendLogin\Validation\RedirectUrlValidator;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class ServerRequestHandler
+{
+    /**
+     * @var RedirectUrlValidator
+     */
+    protected $redirectUrlValidator;
+
+    /**
+     * @var ServerRequestInterface
+     */
+    protected $request;
+
+    public function __construct()
+    {
+        // todo: refactor when extbase handles PSR-15 requests
+        $this->request = $GLOBALS['TYPO3_REQUEST'];
+        $this->redirectUrlValidator = GeneralUtility::makeInstance(
+            RedirectUrlValidator::class,
+            GeneralUtility::makeInstance(SiteFinder::class),
+            (int)$GLOBALS['TSFE']->id
+        );
+    }
+
+    /**
+     * Returns a property that exists in post or get context
+     *
+     * @param string $propertyName
+     * @return mixed|null
+     */
+    public function getPropertyFromGetAndPost(string $propertyName)
+    {
+        return $this->request->getParsedBody()[$propertyName] ?? $this->request->getQueryParams(
+            )[$propertyName] ?? null;
+    }
+
+    /**
+     * Returns validated redirect url cointained in request param return_url or redirect_url
+     *
+     * @return string
+     */
+    public function getRedirectUrlRequestParam(): string
+    {
+        // If config.typolinkLinkAccessRestrictedPages is set, the var is return_url
+        $redirectUrl = (string)$this->getPropertyFromGetAndPost('return_url')
+            ?: (string)$this->getPropertyFromGetAndPost('redirect_url');
+
+        return $this->redirectUrlValidator->isValid($redirectUrl) ? $redirectUrl : '';
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Service/RecoveryService.php b/typo3/sysext/felogin/Classes/Service/RecoveryService.php
new file mode 100644 (file)
index 0000000..66a8a55
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Mail\Mailer;
+use TYPO3\CMS\Core\Mail\MailMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
+use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException;
+use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
+use TYPO3\CMS\FrontendLogin\Configuration\IncompleteConfigurationException;
+use TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
+use TYPO3\CMS\FrontendLogin\Event\SendRecoveryEmailEvent;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class RecoveryService implements RecoveryServiceInterface
+{
+    /**
+     * @var RecoveryConfiguration
+     */
+    protected $recoveryConfiguration;
+
+    /**
+     * @var EventDispatcherInterface
+     */
+    protected $eventDispatcher;
+
+    /**
+     * @var Mailer
+     */
+    protected $mailer;
+
+    /**
+     * @var array
+     */
+    protected $settings;
+
+    /**
+     * @var UriBuilder
+     */
+    protected $uriBuilder;
+
+    /**
+     * @var FrontendUserRepository
+     */
+    protected $userRepository;
+
+    /**
+     * @var LanguageService
+     */
+    protected $languageService;
+
+    /**
+     * @param Mailer $mailer
+     * @param EventDispatcherInterface $eventDispatcher
+     * @param ConfigurationManager $configurationManager
+     * @param RecoveryConfiguration $recoveryConfiguration
+     * @param UriBuilder $uriBuilder
+     * @param FrontendUserRepository $userRepository
+     * @param LanguageService $languageService
+     * @throws InvalidConfigurationTypeException
+     */
+    public function __construct(
+        Mailer $mailer,
+        EventDispatcherInterface $eventDispatcher,
+        ConfigurationManager $configurationManager,
+        RecoveryConfiguration $recoveryConfiguration,
+        UriBuilder $uriBuilder,
+        FrontendUserRepository $userRepository,
+        LanguageService $languageService
+    ) {
+        $this->mailer = $mailer;
+        $this->eventDispatcher = $eventDispatcher;
+        $this->settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS);
+        $this->recoveryConfiguration = $recoveryConfiguration;
+        $this->uriBuilder = $uriBuilder;
+        $this->userRepository = $userRepository;
+        $this->languageService = $languageService;
+    }
+
+    /**
+     * Sends an email with an absolute link including a forgot hash to the passed email address
+     * with instructions to recover the account.
+     *
+     * @param string $emailAddress Receiver's email address.
+     *
+     * @throws TransportExceptionInterface
+     * @throws IncompleteConfigurationException
+     */
+    public function sendRecoveryEmail(string $emailAddress): void
+    {
+        $hash = $this->recoveryConfiguration->getForgotHash();
+        $this->userRepository->updateForgotHashForUserByEmail($emailAddress, $hash);
+        $userInformation = $this->userRepository->fetchUserInformationByEmail($emailAddress);
+        $receiver = new Address($emailAddress, $this->getReceiverName($userInformation));
+        $email = $this->prepareMail($receiver, $hash);
+
+        $event = new SendRecoveryEmailEvent($email, $userInformation);
+        $this->eventDispatcher->dispatch($event);
+        $this->mailer->send($event->getEmail());
+    }
+
+    /**
+     * Get display name from values. Fallback to username if none of the "_name" fields is set.
+     *
+     * @param array $userInformation
+     *
+     * @return string
+     */
+    protected function getReceiverName(array $userInformation): string
+    {
+        $displayName = trim(
+            sprintf(
+                '%s%s%s',
+                $userInformation['first_name'],
+                $userInformation['middle_name'] ? " {$userInformation['middle_name']}" : '',
+                $userInformation['last_name'] ? " {$userInformation['last_name']}" : ''
+            )
+        );
+
+        return $displayName ?: $userInformation['username'];
+    }
+
+    /**
+     * Create email object from configuration.
+     *
+     * @param Address $receiver
+     * @param string $hash
+     * @return Email
+     * @throws IncompleteConfigurationException
+     */
+    protected function prepareMail(Address $receiver, string $hash): Email
+    {
+        $url = $this->uriBuilder->setCreateAbsoluteUri(true)
+            ->uriFor(
+                'showChangePassword',
+                ['hash' => $hash],
+                'PasswordRecovery',
+                'felogin',
+                'Login'
+            );
+
+        $variables = [
+            'receiverName' => $receiver->getName(),
+            'url' => $url,
+            'validUntil' => date($this->settings['dateFormat'], $this->recoveryConfiguration->getLifeTimeTimestamp()),
+        ];
+
+        $plainMailTemplate = $this->recoveryConfiguration->getPlainMailTemplate();
+        $plainMailTemplate->assignMultiple($variables);
+
+        $subject = $this->languageService->sL('LLL:EXT:felogin/Resources/Private/Language/locallang.xlf:password_recovery_mail_header');
+        $mail = GeneralUtility::makeInstance(MailMessage::class);
+        $mail
+            ->subject($subject)
+            ->from($this->recoveryConfiguration->getSender())
+            ->to($receiver)
+            ->text($plainMailTemplate->render());
+
+        if ($this->recoveryConfiguration->hasHtmlMailTemplate()) {
+            $htmlMailTemplate = $this->recoveryConfiguration->getHtmlMailTemplate();
+            $htmlMailTemplate->assignMultiple($variables);
+            $mail->html($htmlMailTemplate->render());
+        }
+
+        $replyTo = $this->recoveryConfiguration->getReplyTo();
+        if ($replyTo) {
+            $mail->addReplyTo($replyTo);
+        }
+
+        return $mail;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Service/RecoveryServiceInterface.php b/typo3/sysext/felogin/Classes/Service/RecoveryServiceInterface.php
new file mode 100644 (file)
index 0000000..7377b18
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException;
+
+interface RecoveryServiceInterface
+{
+    /**
+     * Sends an email with an absolute link including a forgot hash to the passed email address
+     * with instructions to recover the account.
+     *
+     * @param string $emailAddress Receiver's email address.
+     *
+     * @throws InvalidConfigurationTypeException
+     */
+    public function sendRecoveryEmail(string $emailAddress): void;
+}
diff --git a/typo3/sysext/felogin/Classes/Service/UserService.php b/typo3/sysext/felogin/Classes/Service/UserService.php
new file mode 100644 (file)
index 0000000..0b215e5
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class UserService
+{
+    /**
+     * @var bool
+     */
+    protected $userLoggedIn = false;
+
+    /**
+     * @var FrontendUserAuthentication
+     */
+    protected $feUser;
+
+    public function __construct(Context $context)
+    {
+        $this->userLoggedIn = $context->getPropertyFromAspect('frontend.user', 'isLoggedIn');
+        $this->feUser = $GLOBALS['TSFE']->fe_user;
+    }
+
+    /**
+     * Check if the user is logged in
+     *
+     * @return bool
+     */
+    public function isUserLoggedIn(): bool
+    {
+        return $this->userLoggedIn;
+    }
+
+    /**
+     * Get user- and sessiondata from Frontend User
+     *
+     * @return array
+     */
+    public function getFeUserData(): array
+    {
+        return $this->feUser->user;
+    }
+
+    /**
+     * Should return true if a cookie warning is needed to be displayed
+     *
+     * @return bool
+     */
+    public function cookieWarningRequired(): bool
+    {
+        return $this->userLoggedIn && !$this->feUser->isCookieSet();
+    }
+
+    public function getFeUserGroupData(): array
+    {
+        return $this->feUser->groupData;
+    }
+
+    public function getFeUserTable(): string
+    {
+        return $this->feUser->user_table;
+    }
+
+    public function getFeUserGroupTable(): string
+    {
+        return $this->feUser->usergroup_table;
+    }
+
+    public function getFeUserIdColumn(): string
+    {
+        return $this->feUser->userid_column;
+    }
+}
diff --git a/typo3/sysext/felogin/Classes/Service/ValidatorResolverService.php b/typo3/sysext/felogin/Classes/Service/ValidatorResolverService.php
new file mode 100644 (file)
index 0000000..c1392df
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Generator;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+class ValidatorResolverService implements SingletonInterface
+{
+    /**
+     * Resolves Validator classes based on the validator config. This array can either
+     * contain a FQCN or an array with keys "className"(string) and "options"(array).
+     *
+     * @param array $validatorConfig
+     *
+     * @return Generator|null
+     */
+    public function resolve(array $validatorConfig): ?Generator
+    {
+        foreach ($validatorConfig as $validator) {
+            if (is_string($validator)) {
+                yield GeneralUtility::makeInstance($validator);
+            } elseif (is_array($validator)) {
+                yield GeneralUtility::makeInstance($validator['className'], $validator['options']);
+            }
+        }
+    }
+}
index c9d1310..5ec2b96 100644 (file)
@@ -24,10 +24,9 @@ use TYPO3\CMS\Install\Updates\DatabaseUpdatedPrerequisite;
 use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
 
 /**
- * Class MigrateFeloginPlugins
- * @internal
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
  */
-class MigrateFeloginPlugins implements UpgradeWizardInterface
+final class MigrateFeloginPlugins implements UpgradeWizardInterface
 {
     /**
      * @var array Flexform fields which we are interested in updating
diff --git a/typo3/sysext/felogin/Classes/Updates/MigrateFeloginPluginsCtype.php b/typo3/sysext/felogin/Classes/Updates/MigrateFeloginPluginsCtype.php
new file mode 100644 (file)
index 0000000..d05513c
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Updates;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Configuration\Features;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Updates\RepeatableInterface;
+use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
+
+/**
+ * @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
+ */
+final class MigrateFeloginPluginsCtype implements UpgradeWizardInterface, RepeatableInterface
+{
+    private const CTYPE_PIBASE = 'login';
+    private const CTYPE_EXTBASE = 'felogin_login';
+
+    /**
+     * Return the identifier for this wizard
+     * This should be the same string as used in the ext_localconf class registration
+     *
+     * @return string
+     */
+    public function getIdentifier(): string
+    {
+        return self::class;
+    }
+
+    /**
+     * Return the speaking name of this wizard
+     *
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return 'Migrate felogin plugins to use extbase CType';
+    }
+
+    /**
+     * Return the description for this wizard
+     *
+     * @return string
+     */
+    public function getDescription(): string
+    {
+        return 'This wizard migrates existing front end plugins of the extension felogin from piBase key to ' .
+            'the new Extbase "CType"';
+    }
+
+    /**
+     * Execute the update
+     *
+     * Called when a wizard reports that an update is necessary
+     *
+     * @return bool
+     */
+    public function executeUpdate(): bool
+    {
+        // Get all tt_content data for login plugins and update their CTypes and Flexforms settings
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
+
+        /** @var QueryBuilder $queryBuilder */
+        $queryBuilder = $connection->createQueryBuilder();
+        $queryBuilder
+            ->update('tt_content')
+            ->set('CType', $this->getNewCType())
+            ->where(
+                $queryBuilder->expr()->eq(
+                    'CType',
+                    $queryBuilder->createNamedParameter($this->getOldCType())
+                )
+            )
+            ->execute();
+
+        return true;
+    }
+
+    /**
+     * Is an update necessary?
+     *
+     * If the feature toggle is set: Looks for new fe plugins to be rolled back
+     * Otherwise looks for old record sets to be migrated
+     *
+     * @return bool
+     */
+    public function updateNecessary(): bool
+    {
+        /** @var QueryBuilder $queryBuilder */
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
+        $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        $elementCount = $queryBuilder->count('uid')
+            ->from('tt_content')
+            ->where(
+                $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter($this->getOldCType()))
+            )
+            ->execute()->fetchColumn();
+
+        return (bool)$elementCount;
+    }
+
+    /**
+     * Returns an array of class names of Prerequisite classes
+     *
+     * This way a wizard can define dependencies like "database up-to-date" or
+     * "reference index updated"
+     *
+     * @return string[]
+     */
+    public function getPrerequisites(): array
+    {
+        return [
+            MigrateFeloginPlugins::class
+        ];
+    }
+
+    /**
+     * Checks if feature toggle to use extbase version is enabled
+     *
+     * @return bool
+     */
+    protected function isExtbaseFeatureEnabled(): bool
+    {
+        return GeneralUtility::makeInstance(Features::class)
+            ->isFeatureEnabled('felogin.extbase');
+    }
+
+    /**
+     * Returns the CType that should be replaced by new CType
+     *
+     * @return string
+     */
+    private function getOldCType(): string
+    {
+        return $this->isExtbaseFeatureEnabled() ? self::CTYPE_PIBASE : self::CTYPE_EXTBASE;
+    }
+
+    /**
+     * Decide which content CType should be used for the current feature toggle state
+     *
+     * @return string
+     */
+    private function getNewCType(): string
+    {
+        return $this->isExtbaseFeatureEnabled() ? self::CTYPE_EXTBASE : self::CTYPE_PIBASE;
+    }
+}
index 62e3168..f4f3a5b 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 declare(strict_types = 1);
 
-namespace TYPO3\CMS\Felogin\Validation;
+namespace TYPO3\CMS\FrontendLogin\Validation;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 /**
  * Used to check if a referrer or a redirect URL is valid to be used as within Frontend Logins
  * for redirects.
+ *
  * @internal for now as it might get adopted for further streamlining against other validation paradigms
  */
 class RedirectUrlValidator implements LoggerAwareInterface
index e368385..69abfe1 100644 (file)
@@ -4,5 +4,8 @@ services:
     autoconfigure: true
     public: false
 
-  TYPO3\CMS\Felogin\:
-    resource: '../Classes/*'
+  TYPO3\CMS\FrontendLogin\Service\:
+    resource: '../Classes/Service/*'
+
+  TYPO3\CMS\FrontendLogin\Domain\Repository\:
+    resource: '../Classes/Domain/Repository/*'
index 2886b28..0642abf 100644 (file)
@@ -1,21 +1,34 @@
 <?php
 defined('TYPO3_MODE') or die();
 
-call_user_func(function () {
+(static function (): void {
+    $feloginExtbase = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\Features::class)
+        ->isFeatureEnabled('felogin.extbase');
+
+    if ($feloginExtbase) {
+        // Extbase plugin has a different CType
+        $contentTypeName = 'felogin_login';
+        \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
+            'Felogin',
+            'Login',
+            'Login Form'
+        );
+    } else {
+        $contentTypeName = 'login';
+    }
     // Add the FlexForm
     \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
         '*',
         'FILE:EXT:felogin/Configuration/FlexForms/Login.xml',
-        'login'
+        $contentTypeName
     );
-
-    $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes']['login'] = 'mimetypes-x-content-login';
+    $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes'][$contentTypeName] = 'mimetypes-x-content-login';
 
     // check if there is already a forms tab and add the item after that, otherwise
     // add the tab item as well
     $additionalCTypeItem = [
         'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.I.10',
-        'login',
+        $contentTypeName,
         'content-elements-login'
     ];
 
@@ -32,7 +45,12 @@ call_user_func(function () {
 
     if ($groupFound && $groupPosition) {
         // add the new CType item below CType
-        array_splice($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'], $groupPosition, 0, [0 => $additionalCTypeItem]);
+        array_splice(
+            $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'],
+            $groupPosition,
+            0,
+            [0 => $additionalCTypeItem]
+        );
     } else {
         // nothing found, add two items (group + new CType) at the bottom of the list
         \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
@@ -40,10 +58,14 @@ call_user_func(function () {
             'CType',
             ['LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.div.forms', '--div--']
         );
-        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem('tt_content', 'CType', $additionalCTypeItem);
+        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
+            'tt_content',
+            'CType',
+            $additionalCTypeItem
+        );
     }
 
-    $GLOBALS['TCA']['tt_content']['types']['login']['showitem'] = '
+    $GLOBALS['TCA']['tt_content']['types'][$contentTypeName]['showitem'] = '
         --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
             --palette--;;general,
             --palette--;;headers,
@@ -62,4 +84,4 @@ call_user_func(function () {
             rowDescription,
         --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
     ';
-});
+})();
index 2437e09..a60b73d 100644 (file)
@@ -1,11 +1,13 @@
 mod.wizards.newContentElement.wizardItems.forms {
-  elements.login {
-    iconIdentifier = content-elements-login
-    title = LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_title
-    description = LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_description
-    tt_content_defValues {
-      CType = login
+  elements {
+    felogin_login {
+      iconIdentifier = content-elements-login
+      title = LLL:EXT:felogin/Resources/Private/Language/Database.xlf:tt_content.CType.felogin_login.title
+      description = LLL:EXT:felogin/Resources/Private/Language/Database.xlf:tt_content.CType.felogin_login.description
+      tt_content_defValues {
+        CType = felogin_login
+      }
     }
   }
-  show := addToList(login)
+  show := addToList(felogin_login)
 }
diff --git a/typo3/sysext/felogin/Configuration/TsConfig/Page/PiBase/Mod/Wizards/NewContentElement.tsconfig b/typo3/sysext/felogin/Configuration/TsConfig/Page/PiBase/Mod/Wizards/NewContentElement.tsconfig
new file mode 100644 (file)
index 0000000..2437e09
--- /dev/null
@@ -0,0 +1,11 @@
+mod.wizards.newContentElement.wizardItems.forms {
+  elements.login {
+    iconIdentifier = content-elements-login
+    title = LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_title
+    description = LLL:EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf:forms_login_description
+    tt_content_defValues {
+      CType = login
+    }
+  }
+  show := addToList(login)
+}
diff --git a/typo3/sysext/felogin/Configuration/TypoScript/PiBase/constants.typoscript b/typo3/sysext/felogin/Configuration/TypoScript/PiBase/constants.typoscript
new file mode 100644 (file)
index 0000000..2815721
--- /dev/null
@@ -0,0 +1,4 @@
+styles.content.loginform {
+  # cat = Frontend Login/02_Template/100; type=string; label= Login template: Enter the path for the HTML template to be used
+  templateFile = EXT:felogin/Resources/Private/Templates/FrontendLogin.html
+}
diff --git a/typo3/sysext/felogin/Configuration/TypoScript/PiBase/setup.typoscript b/typo3/sysext/felogin/Configuration/TypoScript/PiBase/setup.typoscript
new file mode 100644 (file)
index 0000000..9775695
--- /dev/null
@@ -0,0 +1,104 @@
+# Setting "felogin" plugin TypoScript
+plugin.tx_felogin_pi1 = USER_INT
+plugin.tx_felogin_pi1 {
+  userFunc = TYPO3\CMS\Felogin\Controller\FrontendLoginController->main
+
+  # Storage
+  storagePid = {$styles.content.loginform.pid}
+  recursive = {$styles.content.loginform.recursive}
+
+  # Template
+  templateFile = {$styles.content.loginform.templateFile}
+  feloginBaseURL = {$styles.content.loginform.feloginBaseURL}
+  dateFormat = {$styles.content.loginform.dateFormat}
+
+  # Features
+  showForgotPasswordLink = {$styles.content.loginform.showForgotPasswordLink}
+  showPermaLogin = {$styles.content.loginform.showPermaLogin}
+  showLogoutFormAfterLogin = {$styles.content.loginform.showLogoutFormAfterLogin}
+
+  # E-Mail Settings
+  email_from = {$styles.content.loginform.emailFrom}
+  email_fromName = {$styles.content.loginform.emailFromName}
+  replyTo = {$styles.content.loginform.replyToEmail}
+
+  # Redirects
+  redirectMode = {$styles.content.loginform.redirectMode}
+  redirectFirstMethod = {$styles.content.loginform.redirectFirstMethod}
+  redirectPageLogin = {$styles.content.loginform.redirectPageLogin}
+  redirectPageLoginError = {$styles.content.loginform.redirectPageLoginError}
+  redirectPageLogout = {$styles.content.loginform.redirectPageLogout}
+  redirectDisable = {$styles.content.loginform.redirectDisable}
+
+  # Security
+  forgotLinkHashValidTime = {$styles.content.loginform.forgotLinkHashValidTime}
+  newPasswordMinLength = {$styles.content.loginform.newPasswordMinLength}
+  domains = {$styles.content.loginform.domains}
+  exposeNonexistentUserInForgotPasswordDialog = {$styles.content.loginform.exposeNonexistentUserInForgotPasswordDialog}
+
+  # should a wrapper class be set for this content element
+  wrapContentInBaseClass = 1
+
+  # typolink-configuration for links / urls
+  # parameter and additionalParams are set by extension
+  linkConfig {
+    target =
+    ATagParams = rel="nofollow"
+  }
+
+  # preserve GET vars - define "all" or comma separated list of GET-vars that should be included by link generation
+  preserveGETvars = all
+
+  welcomeHeader_stdWrap {
+    required = 1
+    wrap = <h3>|</h3>
+    htmlSpecialChars = 1
+  }
+
+  successHeader_stdWrap < .welcomeHeader_stdWrap
+  logoutHeader_stdWrap < .welcomeHeader_stdWrap
+  errorHeader_stdWrap < .welcomeHeader_stdWrap
+  forgotHeader_stdWrap < .welcomeHeader_stdWrap
+  changePasswordHeader_stdWrap < .welcomeHeader_stdWrap
+
+  welcomeMessage_stdWrap {
+    required = 1
+    wrap = <div>|</div>
+    htmlSpecialChars = 1
+  }
+
+  successMessage_stdWrap < .welcomeMessage_stdWrap
+  logoutMessage_stdWrap < .welcomeMessage_stdWrap
+  errorMessage_stdWrap < .welcomeMessage_stdWrap
+  forgotMessage_stdWrap < .welcomeMessage_stdWrap
+  forgotErrorMessage_stdWrap < .welcomeMessage_stdWrap
+  forgotResetMessageEmailSentMessage_stdWrap < .welcomeMessage_stdWrap
+  changePasswordNotValidMessage_stdWrap < .welcomeMessage_stdWrap
+  changePasswordTooShortMessage_stdWrap < .welcomeMessage_stdWrap
+  changePasswordNotEqualMessage_stdWrap < .welcomeMessage_stdWrap
+  changePasswordMessage_stdWrap < .welcomeMessage_stdWrap
+  changePasswordDoneMessage_stdWrap < .welcomeMessage_stdWrap
+
+  cookieWarning_stdWrap {
+    required = 1
+    wrap = <p style="color:red; font-weight:bold;">|</p>
+    htmlSpecialChars = 1
+  }
+
+  # stdWrap for fe_users fields used in Messages
+  userfields {
+    username {
+      htmlSpecialChars = 1
+      wrap = <strong>|</strong>
+    }
+  }
+}
+
+# Setting "felogin" plugin TypoScript
+tt_content.login =< lib.contentElement
+tt_content.login {
+  templateName = Generic
+  variables {
+    content =< plugin.tx_felogin_pi1
+  }
+}
index 4d173ee..320a4d9 100644 (file)
@@ -12,7 +12,7 @@ styles.content.loginform {
   recursive = 0
 
   # cat=Frontend Login/02_Template/100; type=string; label= Login template: Enter the path for the HTML template to be used
-  templateFile = EXT:felogin/Resources/Private/Templates/FrontendLogin.html
+  templateFile = EXT:felogin/Resources/Private/Templates/Login/Login.html
   # cat=Frontend Login/02_Template/101; type=string; label= BaseURL for generated links: Base url if something other than the system base URL is needed
   feloginBaseURL =
   # cat=Frontend Login/02_Template/102; type=string; label= Date format: Format for the link is valid until message (forget password email)
@@ -31,6 +31,10 @@ styles.content.loginform {
   emailFromName =
   # cat=Frontend Login/04_EMail/102; type=string; label= Reply To E-Mail Address: Reply-to address used in the change password emails
   replyToEmail =
+  # cat=Frontend Login/04_EMail/103; type=string; label= HTML-email template path: Path to template file used for HTML-Emails
+  emailHtmlTemplatePath = EXT:felogin/Resources/Private/Email/Templates/PasswordRecovery.html
+  # cat=Frontend Login/04_EMail/104; type=string; label= Plain text email template path: Path to template file used for plain text emails
+  emailPlainTemplatePath = EXT:felogin/Resources/Private/Email/Templates/PasswordRecovery.txt
 
   # cat=Frontend Login/05_Redirects/101; type=string; label= Redirect Mode: Comma separated list of redirect modes. Possible values: groupLogin, userLogin, login, getpost, referer, refererDomains, loginError, logout
   redirectMode =
index 9775695..ac1cd8a 100644 (file)
-# Setting "felogin" plugin TypoScript
-plugin.tx_felogin_pi1 = USER_INT
-plugin.tx_felogin_pi1 {
-  userFunc = TYPO3\CMS\Felogin\Controller\FrontendLoginController->main
-
-  # Storage
-  storagePid = {$styles.content.loginform.pid}
-  recursive = {$styles.content.loginform.recursive}
-
-  # Template
-  templateFile = {$styles.content.loginform.templateFile}
-  feloginBaseURL = {$styles.content.loginform.feloginBaseURL}
-  dateFormat = {$styles.content.loginform.dateFormat}
-
-  # Features
-  showForgotPasswordLink = {$styles.content.loginform.showForgotPasswordLink}
-  showPermaLogin = {$styles.content.loginform.showPermaLogin}
-  showLogoutFormAfterLogin = {$styles.content.loginform.showLogoutFormAfterLogin}
-
-  # E-Mail Settings
-  email_from = {$styles.content.loginform.emailFrom}
-  email_fromName = {$styles.content.loginform.emailFromName}
-  replyTo = {$styles.content.loginform.replyToEmail}
-
-  # Redirects
-  redirectMode = {$styles.content.loginform.redirectMode}
-  redirectFirstMethod = {$styles.content.loginform.redirectFirstMethod}
-  redirectPageLogin = {$styles.content.loginform.redirectPageLogin}
-  redirectPageLoginError = {$styles.content.loginform.redirectPageLoginError}
-  redirectPageLogout = {$styles.content.loginform.redirectPageLogout}
-  redirectDisable = {$styles.content.loginform.redirectDisable}
-
-  # Security
-  forgotLinkHashValidTime = {$styles.content.loginform.forgotLinkHashValidTime}
-  newPasswordMinLength = {$styles.content.loginform.newPasswordMinLength}
-  domains = {$styles.content.loginform.domains}
-  exposeNonexistentUserInForgotPasswordDialog = {$styles.content.loginform.exposeNonexistentUserInForgotPasswordDialog}
-
-  # should a wrapper class be set for this content element
-  wrapContentInBaseClass = 1
-
-  # typolink-configuration for links / urls
-  # parameter and additionalParams are set by extension
-  linkConfig {
-    target =
-    ATagParams = rel="nofollow"
-  }
-
-  # preserve GET vars - define "all" or comma separated list of GET-vars that should be included by link generation
-  preserveGETvars = all
-
-  welcomeHeader_stdWrap {
-    required = 1
-    wrap = <h3>|</h3>
-    htmlSpecialChars = 1
-  }
-
-  successHeader_stdWrap < .welcomeHeader_stdWrap
-  logoutHeader_stdWrap < .welcomeHeader_stdWrap
-  errorHeader_stdWrap < .welcomeHeader_stdWrap
-  forgotHeader_stdWrap < .welcomeHeader_stdWrap
-  changePasswordHeader_stdWrap < .welcomeHeader_stdWrap
-
-  welcomeMessage_stdWrap {
-    required = 1
-    wrap = <div>|</div>
-    htmlSpecialChars = 1
-  }
-
-  successMessage_stdWrap < .welcomeMessage_stdWrap
-  logoutMessage_stdWrap < .welcomeMessage_stdWrap
-  errorMessage_stdWrap < .welcomeMessage_stdWrap
-  forgotMessage_stdWrap < .welcomeMessage_stdWrap
-  forgotErrorMessage_stdWrap < .welcomeMessage_stdWrap
-  forgotResetMessageEmailSentMessage_stdWrap < .welcomeMessage_stdWrap
-  changePasswordNotValidMessage_stdWrap < .welcomeMessage_stdWrap
-  changePasswordTooShortMessage_stdWrap < .welcomeMessage_stdWrap
-  changePasswordNotEqualMessage_stdWrap < .welcomeMessage_stdWrap
-  changePasswordMessage_stdWrap < .welcomeMessage_stdWrap
-  changePasswordDoneMessage_stdWrap < .welcomeMessage_stdWrap
-
-  cookieWarning_stdWrap {
-    required = 1
-    wrap = <p style="color:red; font-weight:bold;">|</p>
-    htmlSpecialChars = 1
-  }
-
-  # stdWrap for fe_users fields used in Messages
-  userfields {
-    username {
-      htmlSpecialChars = 1
-      wrap = <strong>|</strong>
+plugin.tx_felogin_login {
+  settings {
+    showPermaLogin = {$styles.content.loginform.showPermaLogin}
+
+    # Template
+    dateFormat = {$styles.content.loginform.dateFormat}
+
+    # E-Mail Settings
+    email_from = {$styles.content.loginform.emailFrom}
+    email_fromName = {$styles.content.loginform.emailFromName}
+    email_htmlTemplatePath = {$styles.content.loginform.emailHtmlTemplatePath}
+    email_plainTemplatePath = {$styles.content.loginform.emailPlainTemplatePath}
+    replyTo = {$styles.content.loginform.replyToEmail}
+
+    # Security
+    forgotLinkHashValidTime = {$styles.content.loginform.forgotLinkHashValidTime}
+    newPasswordMinLength = {$styles.content.loginform.newPasswordMinLength}
+    passwordValidators {
+      10 = TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+      20 {
+        className = TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator
+        options {
+          minimum = {$styles.content.loginform.newPasswordMinLength}
+        }
+      }
     }
   }
 }
-
-# Setting "felogin" plugin TypoScript
-tt_content.login =< lib.contentElement
-tt_content.login {
-  templateName = Generic
-  variables {
-    content =< plugin.tx_felogin_pi1
-  }
-}
diff --git a/typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.html b/typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.html
new file mode 100644 (file)
index 0000000..8130bd3
--- /dev/null
@@ -0,0 +1,13 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+<f:spaceless>
+    <f:variable name="recoveryLink">
+        <f:link.external uri="{url}" target="_blank">Password recovery link</f:link.external>
+    </f:variable>
+
+    {f:translate(
+    key: 'forgot_validate_reset_password_html',
+    extensionName: 'felogin',
+    arguments: { 0: receiverName, 1: recoveryLink, 2: validUntil }
+    ) -> f:format.html()}
+</f:spaceless>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.txt b/typo3/sysext/felogin/Resources/Private/Email/Templates/PasswordRecovery.txt
new file mode 100644 (file)
index 0000000..59f8579
--- /dev/null
@@ -0,0 +1,5 @@
+{f:translate(
+    key: 'forgot_validate_reset_password_plaintext',
+    extensionName: 'felogin',
+    arguments: {0: receiverName, 1: url, 2: validUntil}
+) -> f:format.raw()}
index 6a7552c..231c5c8 100644 (file)
@@ -6,6 +6,12 @@
                        <trans-unit id="tt_content.CType_pi1" resname="tt_content.CType_pi1">
                                <source>Website User Login</source>
                        </trans-unit>
+                       <trans-unit id="tt_content.CType.felogin_login.title" resname="tt_content.CType.felogin_login.title">
+                               <source>Login Form</source>
+                       </trans-unit>
+                       <trans-unit id="tt_content.CType.felogin_login.description" resname="tt_content.CType.felogin_login.description">
+                               <source>Login/logout form used to password protect pages allowing only authorised website users and groups access.</source>
+                       </trans-unit>
                        <trans-unit id="felogin_redirectPid" resname="felogin_redirectPid">
                                <source>Redirect at Login to Page (felogin)</source>
                        </trans-unit>
index 9aac1cc..1833d66 100644 (file)
@@ -3,6 +3,9 @@
        <file t3:id="1415814821" source-language="en" datatype="plaintext" original="EXT:felogin/Resources/Private/Language/locallang.xlf" date="2011-10-17T20:22:32Z" product-name="felogin">
                <header/>
                <body>
+                       <trans-unit id="login_user_info" resname="login_user_info">
+                               <source>You are now logged in as '%s'</source>
+                       </trans-unit>
                        <trans-unit id="welcome_header" resname="welcome_header">
                                <source>User login</source>
                        </trans-unit>
@@ -138,6 +141,33 @@ For security reasons, this link is only active until %s. If you do not visit the
                        <trans-unit id="enter_your_data" resname="enter_your_data">
                                <source>Username or email address</source>
                        </trans-unit>
+                       <trans-unit id="password_recovery_mail_header" resname="password_recovery_mail_header">
+                               <source>Your new password</source>
+                       </trans-unit>
+                       <trans-unit id="password_recovery_link_expired" resname="password_recovery_link_expired">
+                               <source>Your password recovery link is expired.</source>
+                       </trans-unit>
+                       <trans-unit id="empty_password_and_password_repeat" resname="empty_password_and_password_repeat">
+                               <source>New password and new password repeat cannot be empty</source>
+                       </trans-unit>
+                       <trans-unit id="password_must_match_repeated" resname="password_must_match_repeated">
+                               <source>New Password must match repeated password.</source>
+                       </trans-unit>
+                       <trans-unit id="forgot_validate_reset_password_html" resname="forgot_validate_reset_password_html" xml:space="preserve">
+                               <source>&lt;p&gt;Dear %s,&lt;/p&gt;
+&lt;p&gt;This email was sent in response to your request to reset your password. Please click on the link below.&lt;/p&gt;
+&lt;p&gt;%s&lt;/p&gt;
+&lt;p&gt;For security reasons, this link is only active until %s. If you do not visit the link before then, you will need to repeat the password reset steps.&lt;/p&gt;
+                               </source>
+                       </trans-unit>
+                       <trans-unit id="forgot_validate_reset_password_plaintext" resname="forgot_validate_reset_password" xml:space="preserve">
+                               <source>Dear %s,
+
+This email was sent in response to your request to reset your password. Please click on the link below.
+%s
+
+For security reasons, this link is only active until %s. If you do not visit the link before then, you will need to repeat the password reset steps.</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
diff --git a/typo3/sysext/felogin/Resources/Private/Partials/CookieWarning.html b/typo3/sysext/felogin/Resources/Private/Partials/CookieWarning.html
new file mode 100644 (file)
index 0000000..8de8771
--- /dev/null
@@ -0,0 +1,5 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<p style="color:red; font-weight:bold;"><f:translate key="cookie_warning" /></p>
+
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Partials/RenderLabelOrMessage.html b/typo3/sysext/felogin/Resources/Private/Partials/RenderLabelOrMessage.html
new file mode 100644 (file)
index 0000000..3699bc4
--- /dev/null
@@ -0,0 +1,14 @@
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" data-namespace-typo3-fluid="true"
+        xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+        xmlns:fluid="http://typo3.org/ns/TYPO3Fluid/Fluid/ViewHelpers">
+<fluid:spaceless>
+    <f:if condition="{settings.{key}}">
+        <f:then>
+            {settings.{key}}
+        </f:then>
+        <f:else>
+            <f:translate key="{key}"/>
+        </f:else>
+    </f:if>
+</fluid:spaceless>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Partials/ValidationErrors.html b/typo3/sysext/felogin/Resources/Private/Partials/ValidationErrors.html
new file mode 100644 (file)
index 0000000..102ff4c
--- /dev/null
@@ -0,0 +1,17 @@
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" data-namespace-typo3-fluid="true">
+<f:form.validationResults>
+    <f:if condition="{validationResults.flattenedErrors}">
+        <ul>
+            <f:for each="{validationResults.flattenedErrors}" as="errors" key="propertyPath">
+                <li>{propertyPath}
+                    <ul>
+                        <f:for each="{errors}" as="error">
+                            <li>{error.code}: {error}</li>
+                        </f:for>
+                    </ul>
+                </li>
+            </f:for>
+        </ul>
+    </f:if>
+</f:form.validationResults>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Templates/Login/Login.html b/typo3/sysext/felogin/Resources/Private/Templates/Login/Login.html
new file mode 100644 (file)
index 0000000..1bb9455
--- /dev/null
@@ -0,0 +1,95 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:flashMessages/>
+<f:if condition="{cookieWarning}">
+    <f:render partial="CookieWarning" />
+</f:if>
+
+<f:if condition="{messageKey}">
+    <h3>
+        <f:render partial="RenderLabelOrMessage" arguments="{key: '{messageKey}_header'}"/>
+    </h3>
+    <p>
+        <f:render partial="RenderLabelOrMessage" arguments="{key: '{messageKey}_message'}"/>
+    </p>
+</f:if>
+<f:if condition="{onSubmit}">
+    <f:then>
+        <f:form target="_top" fieldNamePrefix="" action="login" onsubmit="{onSubmit}">
+            <f:render section="content" arguments="{_all}"/>
+        </f:form>
+    </f:then>
+    <f:else>
+        <f:form target="_top" fieldNamePrefix="" action="login">
+            <f:render section="content" arguments="{_all}"/>
+        </f:form>
+    </f:else>
+</f:if>
+
+<f:if condition="{settings.showForgotPassword}">
+    <f:link.action action="recovery" controller="PasswordRecovery">
+        <f:render partial="RenderLabelOrMessage" arguments="{key: 'forgot_header'}"/>
+    </f:link.action>
+</f:if>
+
+<f:section name="content">
+    <fieldset>
+        <legend>
+            <f:translate key="login"/>
+        </legend>
+        <div>
+            <label>
+                <f:translate key="username"/>
+                <f:form.textfield name="user"/>
+            </label>
+        </div>
+        <div>
+            <label>
+                <f:translate key="password"/>
+                <f:form.password name="pass" data="{rsa-encryption: ''}"/>
+            </label>
+        </div>
+
+        <f:if condition="{permaloginStatus} > -1">
+            <div>
+                <label>
+                    <f:translate id="permalogin"/>
+                    <f:if condition="{permaloginStatus} == 1">
+                        <f:then>
+                            <f:form.hidden name="permalogin" value="0" additionalAttributes="{disabled: 'disabled'}"/>
+                            <f:form.checkbox name="permalogin" id="permalogin" value="1" checked="checked"/>
+                        </f:then>
+                        <f:else>
+                            <f:form.hidden name="permalogin" value="0"/>
+                            <f:form.checkbox name="permalogin" id="permalogin" value="1"/>
+                        </f:else>
+                    </f:if>
+                </label>
+            </div>
+        </f:if>
+
+        <div>
+            <f:form.submit value="{f:translate(key: 'login')}" name="submit"/>
+        </div>
+
+        <div class="felogin-hidden">
+            <f:form.hidden name="logintype" value="login"/>
+            <f:form.hidden name="pid" value="{storagePid}"/>
+            <f:if condition="{redirectURL}!=''">
+                <f:form.hidden name="redirect_url" value="{redirectURL}" />
+            </f:if>
+            <f:if condition="{referer}!=''">
+                <f:form.hidden name="referer" value="{referer}" />
+            </f:if>
+            <f:if condition="{redirectReferrer}!=''">
+                <f:form.hidden name="redirectReferrer" value="off" />
+            </f:if>
+            <f:if condition="{noRedirect}!=''">
+                <f:form.hidden name="noredirect" value="1" />
+            </f:if>
+
+            {extraHidden}
+        </div>
+    </fieldset>
+</f:section>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Templates/Login/Logout.html b/typo3/sysext/felogin/Resources/Private/Templates/Login/Logout.html
new file mode 100644 (file)
index 0000000..3af478a
--- /dev/null
@@ -0,0 +1,38 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:if condition="{cookieWarning}">
+    <f:render partial="CookieWarning"/>
+</f:if>
+
+<h3>
+    <f:render partial="RenderLabelOrMessage" arguments="{key: 'status_header'}"/>
+</h3>
+<p>
+    <f:render partial="RenderLabelOrMessage" arguments="{key: 'status_message'}"/>
+</p>
+
+<f:form action="login" actionUri="{actionUri}" target="_top" fieldNamePrefix="">
+    <fieldset>
+        <legend>
+            <f:translate key="logout"/>
+        </legend>
+        <div>
+            <label>
+                <f:translate key="username"/>
+            </label>
+            {user.username}
+        </div>
+        <div>
+            <f:form.submit value="{f:translate(key: 'logout')}" name="submit"/>
+        </div>
+
+        <div class="felogin-hidden">
+            <f:form.hidden name="logintype" value="logout"/>
+            <f:form.hidden name="pid" value="{storagePid}"/>
+            <f:comment>
+                <input type="hidden" name="###PREFIXID###[noredirect]" value="###NOREDIRECT###"/>
+            </f:comment>
+        </div>
+    </fieldset>
+</f:form>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Templates/Login/Overview.html b/typo3/sysext/felogin/Resources/Private/Templates/Login/Overview.html
new file mode 100644 (file)
index 0000000..7308b91
--- /dev/null
@@ -0,0 +1,16 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:if condition="{cookieWarning}">
+    <f:render partial="CookieWarning"/>
+</f:if>
+
+<f:if condition="{loginMessage}">
+    <f:then>
+        <h3>
+            <f:render partial="RenderLabelOrMessage" arguments="{key: 'success_header'}"/>
+        </h3>
+    </f:then>
+</f:if>
+
+<f:translate key="login_user_info" arguments="{0: '{user.username}'}"/>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/Recovery.html b/typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/Recovery.html
new file mode 100644 (file)
index 0000000..3e9adf9
--- /dev/null
@@ -0,0 +1,33 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+<h3>
+    <f:translate key="forgot_header"/>
+</h3>
+<p>
+    <f:translate key="forgot_reset_message"/>
+</p>
+
+<f:render partial="ValidationErrors"/>
+
+<f:form action="recovery" method="post">
+    <fieldset>
+        <legend>
+            <f:render partial="RenderLabelOrMessage" arguments="{key: 'reset_password'}"/>
+        </legend>
+        <div>
+            <label>
+                <f:translate key="enter_your_data"/>
+                <f:form.textfield name="userIdentifier"/>
+            </label>
+        </div>
+        <div>
+            <f:form.submit value="{f:translate(key: 'reset_password')}"/>
+        </div>
+    </fieldset>
+</f:form>
+
+<p>
+    <f:link.action action="login" controller="Login">
+        <f:translate key="forgot_header_backToLogin"/>
+    </f:link.action>
+</p>
+</html>
diff --git a/typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/ShowChangePassword.html b/typo3/sysext/felogin/Resources/Private/Templates/PasswordRecovery/ShowChangePassword.html
new file mode 100644 (file)
index 0000000..d635dc6
--- /dev/null
@@ -0,0 +1,36 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+<h3>
+    <f:translate key="change_password_header"/>
+</h3>
+<p>
+    <f:translate key="change_password_message" arguments="{0: settings.newPasswordMinLength}"/>
+</p>
+
+<f:render partial="ValidationErrors"/>
+
+<f:form action="changePassword" method="post">
+    <legend>
+        <f:translate key="change_password"/>
+    </legend>
+    <div>
+        <label>
+            <f:translate key="newpassword_label1"/>
+            <f:form.password name="newPass" additionalAttributes="{autocomplete: 'new-password'}"/>
+        </label>
+    </div>
+    <div>
+        <label>
+            <f:translate key="newpassword_label2"/>
+            <f:form.password name="newPassRepeat" additionalAttributes="{autocomplete: 'new-password'}"/>
+        </label>
+    </div>
+    <f:form.hidden name="hash" value="{hash}"/>
+    <f:form.submit value="{f:translate(key: 'change_password')}"/>
+</f:form>
+
+<p>
+    <f:link.action action="login" controller="Login">
+        <f:translate key="forgot_header_backToLogin"/>
+    </f:link.action>
+</p>
+</html>
diff --git a/typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserGroupRepositoryTest.php b/typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserGroupRepositoryTest.php
new file mode 100644 (file)
index 0000000..54856f3
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Functional\Domain\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserGroupRepository;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class FrontendUserGroupRepositoryTest extends FunctionalTestCase
+{
+    /**
+     * @var array
+     */
+    protected $coreExtensionsToLoad = ['extbase', 'felogin'];
+
+    /**
+     * @var FrontendUserGroupRepository
+     */
+    protected $repository;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $context = new Context();
+        $GLOBALS['TSFE'] = static::getMockBuilder(TypoScriptFrontendController::class)
+            ->disableOriginalConstructor()
+            ->getMock()
+        ;
+
+        $GLOBALS['TSFE']->fe_user = new FrontendUserAuthentication();
+
+        $this->repository = new FrontendUserGroupRepository(
+            new UserService($context),
+            $this->getConnectionPool()
+        );
+
+        $this->importDataSet(__DIR__ . '/../../Fixtures/fe_groups.xml');
+    }
+
+    /**
+     * @test
+     */
+    public function getTable()
+    {
+        self::assertSame('fe_groups', $this->repository->getTable());
+    }
+
+    /**
+     * @test
+     */
+    public function findRedirectPageIdByGroupId()
+    {
+        self::assertNull($this->repository->findRedirectPageIdByGroupId(99));
+        self::assertSame(10, $this->repository->findRedirectPageIdByGroupId(1));
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserRepositoryTest.php b/typo3/sysext/felogin/Tests/Functional/Domain/Repository/FrontendUserRepositoryTest.php
new file mode 100644 (file)
index 0000000..cb1a7c1
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Functional\Domain\Repository;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
+use TYPO3\CMS\FrontendLogin\Service\UserService;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class FrontendUserRepositoryTest extends FunctionalTestCase
+{
+    /**
+     * @var array
+     */
+    protected $coreExtensionsToLoad = ['extbase', 'felogin'];
+
+    /**
+     * @var FrontendUserRepository
+     */
+    protected $repository;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $context = new Context();
+        $GLOBALS['TSFE'] = static::getMockBuilder(TypoScriptFrontendController::class)
+            ->disableOriginalConstructor()
+            ->getMock()
+        ;
+
+        $GLOBALS['TSFE']->fe_user = new FrontendUserAuthentication();
+
+        $this->repository = new FrontendUserRepository(
+            new UserService($context),
+            $this->getConnectionPool(),
+            $context
+        );
+
+        $this->importDataSet(__DIR__ . '/../../Fixtures/fe_users.xml');
+    }
+
+    /**
+     * @test
+     */
+    public function getTable()
+    {
+        self::assertSame('fe_users', $this->repository->getTable());
+    }
+
+    /**
+     * @test
+     */
+    public function findEmailByUsernameOrEmailOnPages()
+    {
+        self::assertNull($this->repository->findEmailByUsernameOrEmailOnPages(''));
+        self::assertNull($this->repository->findEmailByUsernameOrEmailOnPages('non-existent-email-or-username'));
+        self::assertNull($this->repository->findEmailByUsernameOrEmailOnPages('user-with-username-without-email'));
+        self::assertNull($this->repository->findEmailByUsernameOrEmailOnPages('foobar', [99]));
+
+        self::assertSame('foo@bar.baz', $this->repository->findEmailByUsernameOrEmailOnPages('foobar'));
+        self::assertSame('foo@bar.baz', $this->repository->findEmailByUsernameOrEmailOnPages('foo@bar.baz'));
+    }
+
+    /**
+     * @test
+     */
+    public function existsUserWithHash()
+    {
+        self::assertFalse($this->repository->existsUserWithHash('non-existent-hash'));
+        self::assertTrue($this->repository->existsUserWithHash('cf8edd6fa435b4a9fcbb953f81bd84f2'));
+    }
+
+    /**
+     * @test
+     * @dataProvider fetchUserInformationByEmailDataProvider
+     * @param string $emailAddress
+     * @param array $expected
+     */
+    public function fetchUserInformationByEmail(string $emailAddress, array $expected)
+    {
+        // strval() is used since not all of the DBMS return an integer for the "uid" field
+        self::assertSame($expected, array_map('strval', $this->repository->fetchUserInformationByEmail($emailAddress)));
+    }
+
+    public function fetchUserInformationByEmailDataProvider(): array
+    {
+        return [
+            'foo@bar.baz' => [
+                'foo@bar.baz',
+                [
+                    'uid' => '1',
+                    'username' => 'foobar',
+                    'email' => 'foo@bar.baz',
+                    'first_name' => '',
+                    'middle_name' => '',
+                    'last_name' => '',
+                ]
+            ],
+            '' => [
+                '',
+                [
+                    'uid' => '2',
+                    'username' => 'user-with-username-without-email',
+                    'email' => '',
+                    'first_name' => '',
+                    'middle_name' => '',
+                    'last_name' => '',
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     */
+    public function findOneByForgotPasswordHash()
+    {
+        self::assertNull($this->repository->findOneByForgotPasswordHash(''));
+        self::assertNull($this->repository->findOneByForgotPasswordHash('non-existent-hash'));
+        self::assertIsArray($this->repository->findOneByForgotPasswordHash('cf8edd6fa435b4a9fcbb953f81bd84f2'));
+    }
+
+    /**
+     * @test
+     */
+    public function findRedirectIdPageByUserId()
+    {
+        self::assertNull($this->repository->findRedirectIdPageByUserId(99));
+        self::assertSame(10, $this->repository->findRedirectIdPageByUserId(1));
+    }
+
+    /**
+     * @test
+     */
+    public function updateForgotHashForUserByEmail()
+    {
+        $email = 'foo@bar.baz';
+        $newPasswordHash = 'new-hash';
+
+        $this->repository->updateForgotHashForUserByEmail($email, $newPasswordHash);
+
+        $queryBuilder = $this->getConnectionPool()
+            ->getConnectionForTable('fe_users')
+            ->createQueryBuilder();
+
+        $query = $queryBuilder
+            ->select('felogin_forgotHash')
+            ->from('fe_users')
+            ->where($queryBuilder->expr()->eq(
+                'email',
+                $queryBuilder->createNamedParameter($email)
+            ))
+        ;
+
+        self::assertSame($newPasswordHash, $query->execute()->fetchColumn());
+    }
+
+    /**
+     * @test
+     */
+    public function updatePasswordAndInvalidateHash()
+    {
+        $this->repository->updatePasswordAndInvalidateHash('cf8edd6fa435b4a9fcbb953f81bd84f2', 'new-password');
+
+        $queryBuilder = $this->getConnectionPool()
+            ->getConnectionForTable('fe_users')
+            ->createQueryBuilder();
+
+        $query = $queryBuilder
+            ->select('*')
+            ->from('fe_users')
+            ->where($queryBuilder->expr()->eq(
+                'uid',
+                $queryBuilder->createNamedParameter(1)
+            ))
+        ;
+
+        $user = $query->execute()->fetch();
+
+        self::assertSame('new-password', $user['password']);
+        self::assertSame('', $user['felogin_forgotHash']);
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Functional/Fixtures/fe_groups.xml b/typo3/sysext/felogin/Tests/Functional/Fixtures/fe_groups.xml
new file mode 100644 (file)
index 0000000..a80a961
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <fe_groups>
+        <uid>1</uid>
+        <felogin_redirectPid>10</felogin_redirectPid>
+    </fe_groups>
+</dataset>
diff --git a/typo3/sysext/felogin/Tests/Functional/Fixtures/fe_users.xml b/typo3/sysext/felogin/Tests/Functional/Fixtures/fe_users.xml
new file mode 100644 (file)
index 0000000..bdd26c6
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+    <fe_users>
+        <uid>1</uid>
+        <name>Foo Bar</name>
+        <username>foobar</username>
+        <email>foo@bar.baz</email>
+        <felogin_forgotHash>cf8edd6fa435b4a9fcbb953f81bd84f2</felogin_forgotHash>
+        <felogin_redirectPid>10</felogin_redirectPid>
+    </fe_users>
+    <fe_users>
+        <uid>2</uid>
+        <username>user-with-username-without-email</username>
+        <email></email>
+    </fe_users>
+</dataset>
index d406480..f25437d 100644 (file)
@@ -1,5 +1,7 @@
 <?php
-namespace TYPO3\CMS\Felogin\Tests\Functional\Tca;
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Functional\Tca;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -15,13 +17,21 @@ namespace TYPO3\CMS\Felogin\Tests\Functional\Tca;
  */
 
 use TYPO3\CMS\Backend\Tests\Functional\Form\FormTestService;
+use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
-class ContentVisibleFieldsTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
+class ContentVisibleFieldsTest extends FunctionalTestCase
 {
+    /**
+     * @var array
+     */
     protected $coreExtensionsToLoad = ['felogin'];
 
+    /**
+     * @var array
+     */
     protected static $contentFields = [
         'CType',
         'colPos',
@@ -42,8 +52,11 @@ class ContentVisibleFieldsTest extends \TYPO3\TestingFramework\Core\Functional\F
     /**
      * @test
      */
-    public function contentFormContainsExpectedFields()
+    public function piBaseLoginFormContainsExpectedFields(): void
     {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['felogin.extbase'] = false;
+        // Reload TCA now, as the TCA is different based on the feature toggle
+        Bootstrap::loadBaseTca(false);
         $this->setUpBackendUserFromFixture(1);
         $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
 
@@ -57,4 +70,23 @@ class ContentVisibleFieldsTest extends \TYPO3\TestingFramework\Core\Functional\F
             );
         }
     }
+
+    /**
+     * @test
+     */
+    public function loginFormContainsExpectedFields(): void
+    {
+        $this->setUpBackendUserFromFixture(1);
+        $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
+
+        $formEngineTestService = GeneralUtility::makeInstance(FormTestService::class);
+        $formResult = $formEngineTestService->createNewRecordForm('tt_content', ['CType' => 'felogin_login']);
+
+        foreach (static::$contentFields as $expectedField) {
+            self::assertNotFalse(
+                $formEngineTestService->formHtmlContainsField($expectedField, $formResult['html']),
+                'The field ' . $expectedField . ' is not in the form HTML'
+            );
+        }
+    }
 }
diff --git a/typo3/sysext/felogin/Tests/Unit/Configuration/RecoveryConfigurationTest.php b/typo3/sysext/felogin/Tests/Unit/Configuration/RecoveryConfigurationTest.php
new file mode 100644 (file)
index 0000000..9fd3be5
--- /dev/null
@@ -0,0 +1,253 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
+use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException;
+use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\FrontendLogin\Configuration\IncompleteConfigurationException;
+use TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class RecoveryConfigurationTest extends UnitTestCase
+{
+    /**
+     * @var ObjectProphecy|Context
+     */
+    protected $context;
+    /**
+     * @var ObjectProphecy|ConfigurationManager
+     */
+    protected $configurationManager;
+    /**
+     * @var ObjectProphecy|HashService
+     */
+    protected $hashService;
+    /**
+     * @var array
+     */
+    protected $settings = [
+        'email_from' => 'example@example.com',
+        'email_fromName' => 'TYPO3 Installation',
+        'email_plainTemplatePath' => '/some/path/to/a/plain/text/file',
+        'email_htmlTemplatePath' => '/some/path/to/a/html/file',
+        'forgotLinkHashValidTime' => 1,
+        'replyTo' => ''
+    ];
+    /**
+     * @var RecoveryConfiguration
+     */
+    protected $subject;
+    /**
+     * @var ObjectProphecy|LoggerInterface
+     */
+    protected $logger;
+
+    protected function setUp(): void
+    {
+        $this->context = $this->prophesize(Context::class);
+        $this->configurationManager = $this->prophesize(ConfigurationManager::class);
+        $this->hashService = $this->prophesize(HashService::class);
+        $this->logger = $this->prophesize(LoggerInterface::class);
+
+        $this->hashService->generateHmac(Argument::type('string'))->willReturn('some hash');
+
+        parent::setUp();
+    }
+
+    /**
+     * @throws InvalidConfigurationTypeException
+     * @throws IncompleteConfigurationException
+     */
+    protected function setupSubject(): void
+    {
+        $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS)->willReturn($this->settings);
+
+        $this->subject = new RecoveryConfiguration(
+            $this->context->reveal(),
+            $this->configurationManager->reveal(),
+            new Random(),
+            $this->hashService->reveal()
+        );
+
+        $this->subject->setLogger($this->logger->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function hasHtmlMailTemplateShouldReturnFalseAndLogIfNoHtmlTemplatePathIsConfigured(): void
+    {
+        $this->settings['email_htmlTemplatePath'] = '';
+        $this->setupSubject();
+
+        self::assertFalse($this->subject->hasHtmlMailTemplate());
+
+        $this->logger
+            ->warning(
+                'Key "plugin.tx_felogin_login.settings.email_htmlTemplatePath" is empty or unset.',
+                [$this->subject]
+            )
+            ->shouldHaveBeenCalledTimes(1);
+    }
+
+    /**
+     * @test
+     */
+    public function getSenderShouldReturnAddressWithFallbackFromGlobals(): void
+    {
+        $this->settings['email_from'] = null;
+        $this->settings['email_fromName'] = null;
+        $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'no-reply@example.com';
+        $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'] = 'Example Inc.';
+
+        $this->setupSubject();
+
+        $sender = $this->subject->getSender();
+
+        self::assertSame(
+            $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'],
+            $sender->getAddress()
+        );
+        self::assertSame(
+            $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'],
+            $sender->getName()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getSenderShouldReturnAddressWithConfigFromTypoScript(): void
+    {
+        $this->setupSubject();
+
+        $sender = $this->subject->getSender();
+
+        self::assertSame(
+            $this->settings['email_from'],
+            $sender->getAddress()
+        );
+        self::assertSame(
+            $this->settings['email_fromName'],
+            $sender->getName()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getHtmlMailTemplateShouldNotCreateMailTemplateWhilePathIsEmpty(): void
+    {
+        $this->settings['email_htmlTemplatePath'] = '';
+        $this->setupSubject();
+
+        $this->subject->getHtmlMailTemplate();
+    }
+
+    /**
+     * @test
+     */
+    public function getPlainMailTemplateThrowsExceptionIfPlainMailTemplatePathIsEmpty(): void
+    {
+        $this->settings['email_plainTemplatePath'] = '';
+        $this->expectException(IncompleteConfigurationException::class);
+        $this->expectExceptionCode(1562665945);
+        $this->setupSubject();
+        $this->subject->getPlainMailTemplate();
+    }
+
+    /**
+     * @test
+     */
+    public function getPlainMailTemplateCreatesMailTemplateOnce(): void
+    {
+        $mailTemplate = $this->prophesize(StandaloneView::class);
+        GeneralUtility::addInstance(StandaloneView::class, $mailTemplate->reveal());
+        $this->setupSubject();
+
+        $this->subject->getPlainMailTemplate();
+        $this->subject->getPlainMailTemplate();
+
+        $mailTemplate->setTemplatePathAndFilename($this->settings['email_plainTemplatePath'])->shouldHaveBeenCalledTimes(1);
+    }
+
+    /**
+     * @test
+     */
+    public function getHtmlMailTemplateCreatesMailTemplateOnce(): void
+    {
+        $mailTemplate = $this->prophesize(StandaloneView::class);
+        GeneralUtility::addInstance(StandaloneView::class, $mailTemplate->reveal());
+        $this->setupSubject();
+
+        $this->subject->getHtmlMailTemplate();
+        $this->subject->getHtmlMailTemplate();
+
+        $mailTemplate->setTemplatePathAndFilename($this->settings['email_htmlTemplatePath'])->shouldHaveBeenCalledTimes(1);
+    }
+
+    /**
+     * @test
+     */
+    public function getLifeTimeTimestampShouldReturnTimestamp(): void
+    {
+        $timestamp = time();
+        $expected = $timestamp + 3600 * $this->settings['forgotLinkHashValidTime'];
+        $this->context->getPropertyFromAspect('date', 'timestamp')->willReturn($timestamp);
+        $this->setupSubject();
+
+        $actual = $this->subject->getLifeTimeTimestamp();
+
+        $this->context->getPropertyFromAspect('date', 'timestamp')->shouldHaveBeenCalledTimes(1);
+        self::assertSame($expected, $actual);
+    }
+
+    /**
+     * @test
+     */
+    public function getForgotHashShouldReturnHashWithLifeTimeTimestamp(): void
+    {
+        $timestamp = time();
+        $expectedTimestamp = $timestamp + 3600 * $this->settings['forgotLinkHashValidTime'];
+        $expected = "{$expectedTimestamp}|some hash";
+        $this->context->getPropertyFromAspect('date', 'timestamp')->willReturn($timestamp);
+        $this->setupSubject();
+
+        self::assertSame(
+            $expected,
+            $this->subject->getForgotHash()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getReplyToShouldReturnNullIfNoneAreSet(): void
+    {
+        $this->setupSubject();
+
+        self::assertNull($this->subject->getReplyTo());
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Unit/Redirect/RedirectHandlerTest.php b/typo3/sysext/felogin/Tests/Unit/Redirect/RedirectHandlerTest.php
new file mode 100644 (file)
index 0000000..b29e7c7
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Controller;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Generator;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration;
+use TYPO3\CMS\FrontendLogin\Redirect\RedirectHandler;
+use TYPO3\CMS\FrontendLogin\Redirect\RedirectModeHandler;
+use TYPO3\CMS\FrontendLogin\Redirect\ServerRequestHandler;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class RedirectHandlerTest extends UnitTestCase
+{
+    /**
+     * If set to true, tearDown() will purge singleton instances created by the test.
+     *
+     * @var bool
+     */
+    protected $resetSingletonInstances = true;
+
+    /**
+     * @var RedirectHandler
+     */
+    protected $subject;
+
+    /**
+     * @var ServerRequestInterface
+     */
+    protected $typo3Request;
+
+    /**
+     * @var ServerRequestHandler
+     */
+    protected $serverRequestHandler;
+
+    /**
+     * @var RedirectModeHandler
+     */
+    protected $redirectModeHandler;
+
+    /**
+     * @var \Prophecy\Prophecy\ObjectProphecy|Context
+     */
+    protected $context;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->serverRequestHandler = $this->prophesize(ServerRequestHandler::class);
+        $this->redirectModeHandler = $this->prophesize(RedirectModeHandler::class);
+        $this->context = $this->prophesize(Context::class);
+
+        $GLOBALS['TSFE'] = $this->prophesize(TypoScriptFrontendController::class)->reveal();
+
+        $this->subject = new RedirectHandler(
+            $this->serverRequestHandler->reveal(),
+            $this->redirectModeHandler->reveal(),
+            $this->context->reveal()
+        );
+    }
+
+    protected function tearDown(): void
+    {
+        unset($GLOBALS['TSFE'], $GLOBALS['TYPO3_REQUEST']);
+        parent::tearDown();
+    }
+
+    /**
+     * @test
+     * @dataProvider loginTypeLogoutDataProvider
+     * @param string $expect
+     * @param array $settings
+     */
+    public function processShouldReturnStringForLoginTypeLogout(string $expect, string $redirectMode): void
+    {
+        $this->redirectModeHandler->redirectModeLogout(0)->willReturn('');
+
+        self::assertEquals($expect, $this->subject->processRedirect('logout', new RedirectConfiguration($redirectMode, '', 0, '', 0, 0), ''));
+    }
+
+    public function loginTypeLogoutDataProvider(): Generator
+    {
+        yield 'empty string on empty redirect mode' => ['', ''];
+        yield 'empty string on redirect mode logout' => ['', 'logout'];
+    }
+
+    /**
+     * @test
+     * @dataProvider getLogoutRedirectUrlDataProvider
+     * @param string $expected
+     * @param array $redirectModes
+     * @param array $body
+     * @param bool $userLoggedIn
+     */
+    public function getLogoutRedirectUrlShouldReturnAlternativeRedirectUrl(
+        string $expected,
+        array $redirectModes,
+        array $body,
+        bool $userLoggedIn
+    ): void {
+        $this->setUserLoggedIn($userLoggedIn);
+
+        $this->serverRequestHandler
+            ->getRedirectUrlRequestParam()
+            ->willReturn($body['return_url'] ?? '');
+
+        self::assertEquals($expected, $this->subject->getLogoutRedirectUrl($redirectModes));
+    }
+
+    public function getLogoutRedirectUrlDataProvider(): Generator
+    {
+        yield 'empty redirect mode should return empty returnUrl' => ['', [], [], false];
+        yield 'redirect mode getpost should return param return_url' => [
+            'https://dummy.url',
+            ['getpost'],
+            ['return_url' => 'https://dummy.url'],
+            false
+        ];
+        yield 'redirect mode getpost,logout should return param return_url on not logged in user' => [
+            'https://dummy.url/3',
+            ['getpost', 'logout'],
+            ['return_url' => 'https://dummy.url/3'],
+            false
+        ];
+    }
+
+    /**
+     * @test
+     */
+    public function getLogoutRedirectUrlShouldReturnAlternativeRedirectUrlForLoggedInUserAndRedirectPageLogoutSet(
+    ): void {
+        $this->setUserLoggedIn(true);
+
+        $this->subject = new RedirectHandler(
+            $this->serverRequestHandler->reveal(),
+            $this->redirectModeHandler->reveal(),
+            $this->context->reveal()
+        );
+
+        $this->serverRequestHandler
+            ->getRedirectUrlRequestParam()
+            ->willReturn([]);
+
+        $this->redirectModeHandler
+            ->redirectModeLogout(3)
+            ->willReturn('https://logout.url');
+
+        self::assertEquals('https://logout.url', $this->subject->getLogoutRedirectUrl(['logout'], 3));
+    }
+
+    protected function setUserLoggedIn(bool $userLoggedIn): void
+    {
+        $this->context
+            ->getPropertyFromAspect('frontend.user', 'isLoggedIn')
+            ->willReturn($userLoggedIn);
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Unit/Service/RecoveryServiceTest.php b/typo3/sysext/felogin/Tests/Unit/Service/RecoveryServiceTest.php
new file mode 100644 (file)
index 0000000..25616bb
--- /dev/null
@@ -0,0 +1,241 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Generator;
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Mime\Address;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Mail\Mailer;
+use TYPO3\CMS\Core\Mail\MailMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
+use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration;
+use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
+use TYPO3\CMS\FrontendLogin\Service\RecoveryService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class RecoveryServiceTest extends UnitTestCase
+{
+    /**
+     * @var bool
+     */
+    protected $resetSingletonInstances = true;
+
+    /**
+     * @var FrontendUserRepository|ObjectProphecy
+     */
+    protected $userRepository;
+
+    /**
+     * @var RecoveryConfiguration|ObjectProphecy
+     */
+    protected $recoveryConfiguration;
+
+    protected function setUp(): void
+    {
+        $this->userRepository = $this->prophesize(FrontendUserRepository::class);
+        $this->recoveryConfiguration = $this->prophesize(RecoveryConfiguration::class);
+    }
+
+    /**
+     * @test
+     * @dataProvider configurationDataProvider
+     * @param string $emailAddress
+     * @param array $recoveryConfiguration
+     * @param array $userInformation
+     * @param Address $receiver
+     * @param array $settings
+     */
+    public function sendRecoveryEmailShouldGenerateMailFromConfiguration(
+        string $emailAddress,
+        array $recoveryConfiguration,
+        array $userInformation,
+        Address $receiver,
+        array $settings
+    ): void {
+        $expectedMail = new MailMessage();
+        $expectedMail->subject('translation')
+            ->from($recoveryConfiguration['sender'])
+            ->to($receiver)
+            ->text('plain mail template');
+        $expectedViewVariables = [
+            'receiverName' => $receiver->getName(),
+            'url' => 'some uri',
+            'validUntil' => date($settings['dateFormat'], $recoveryConfiguration['lifeTimeTimestamp'])
+        ];
+
+        $plainMailTemplate = $this->prophesize(StandaloneView::class);
+        $plainMailTemplate->assignMultiple($expectedViewVariables)->shouldBeCalledOnce();
+        $plainMailTemplate->render()->willReturn('plain mail template');
+        $htmlMailTemplate = $this->prophesize(StandaloneView::class);
+
+        if ($recoveryConfiguration['hasHtmlMailTemplate']) {
+            $this->recoveryConfiguration->getHtmlMailTemplate()->willReturn($htmlMailTemplate->reveal());
+            $htmlMailTemplate->assignMultiple($expectedViewVariables)->shouldBeCalledOnce();
+            $htmlMailTemplate->render()->willReturn('html mail template');
+            $expectedMail->html('html mail template');
+        }
+
+        $this->mockRecoveryConfigurationAndUserRepository(
+            $emailAddress,
+            $recoveryConfiguration,
+            $userInformation,
+            $plainMailTemplate
+        );
+
+        $configurationManager = $this->prophesize(ConfigurationManager::class);
+        $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS)->willReturn($settings);
+
+        $languageService = $this->prophesize(LanguageService::class);
+        $languageService->sL(Argument::containingString('password_recovery_mail_header'))->willReturn('translation');
+
+        $uriBuilder = $this->prophesize(UriBuilder::class);
+        $uriBuilder->setCreateAbsoluteUri(true)->willReturn($uriBuilder->reveal());
+        $uriBuilder->uriFor(
+            'showChangePassword',
+            ['hash' => $recoveryConfiguration['forgotHash']],
+            'PasswordRecovery',
+            'felogin',
+            'Login'
+        )->willReturn('some uri');
+
+        $mailer = $this->prophesize(Mailer::class);
+
+        GeneralUtility::addInstance(MailMessage::class, new MailMessage());
+
+        $eventDispatcherProphecy = $this->prophesize(EventDispatcherInterface::class);
+        $subject = new RecoveryService(
+            $mailer->reveal(),
+            $eventDispatcherProphecy->reveal(),
+            $configurationManager->reveal(),
+            $this->recoveryConfiguration->reveal(),
+            $uriBuilder->reveal(),
+            $this->userRepository->reveal(),
+            $languageService->reveal()
+        );
+
+        $subject->sendRecoveryEmail($emailAddress);
+
+        $mailer->send($expectedMail)->shouldHaveBeenCalledOnce();
+    }
+
+    public function configurationDataProvider(): Generator
+    {
+        yield 'minimal configuration' => [
+            'email' => 'max@mustermann.de',
+            'recoveryConfiguration' => [
+                'lifeTimeTimestamp' => 1234567899,
+                'forgotHash' => '0123456789|some hash',
+                'sender' => new Address('typo3@typo3.typo3', 'TYPO3 Installation'),
+                'hasHtmlMailTemplate' => false,
+                'replyTo' => null
+            ],
+            'userInformation' => [
+                'first_name' => '',
+                'middle_name' => '',
+                'last_name' => '',
+                'username' => 'm.mustermann'
+            ],
+            'receiver' => new Address('max@mustermann.de', 'm.mustermann'),
+            'settings' => ['dateFormat' => 'Y-m-d H:i']
+        ];
+        yield 'html mail provided' => [
+            'email' => 'max@mustermann.de',
+            'recoveryConfiguration' => [
+                'lifeTimeTimestamp' => 123456789,
+                'forgotHash' => '0123456789|some hash',
+                'sender' => new Address('typo3@typo3.typo3', 'TYPO3 Installation'),
+                'hasHtmlMailTemplate' => true,
+                'replyTo' => null
+            ],
+            'userInformation' => [
+                'first_name' => '',
+                'middle_name' => '',
+                'last_name' => '',
+                'username' => 'm.mustermann'
+            ],
+            'receiver' => new Address('max@mustermann.de', 'm.mustermann'),
+            'settings' => ['dateFormat' => 'Y-m-d H:i']
+        ];
+        yield 'complex display name instead of username' => [
+            'email' => 'max@mustermann.de',
+            'recoveryConfiguration' => [
+                'lifeTimeTimestamp' => 123456789,
+                'forgotHash' => '0123456789|some hash',
+                'sender' => new Address('typo3@typo3.typo3', 'TYPO3 Installation'),
+                'hasHtmlMailTemplate' => true,
+                'replyTo' => null
+            ],
+            'userInformation' => [
+                'first_name' => 'Max',
+                'middle_name' => 'Maximus',
+                'last_name' => 'Mustermann',
+                'username' => 'm.mustermann'
+            ],
+            'receiver' => new Address('max@mustermann.de', 'Max Maximus Mustermann'),
+            'settings' => ['dateFormat' => 'Y-m-d H:i']
+        ];
+        yield 'custom dateFormat and no middle name' => [
+            'email' => 'max@mustermann.de',
+            'recoveryConfiguration' => [
+                'lifeTimeTimestamp' => 987654321,
+                'forgotHash' => '0123456789|some hash',
+                'sender' => new Address('typo3@typo3.typo3', 'TYPO3 Installation'),
+                'hasHtmlMailTemplate' => true,
+                'replyTo' => null
+            ],
+            'userInformation' => [
+                'first_name' => 'Max',
+                'middle_name' => '',
+                'last_name' => 'Mustermann',
+                'username' => 'm.mustermann'
+            ],
+            'receiver' => new Address('max@mustermann.de', 'Max Mustermann'),
+            'settings' => ['dateFormat' => 'Y-m-d']
+        ];
+    }
+
+    /**
+     * @param string $emailAddress
+     * @param array $recoveryConfiguration
+     * @param array $userInformation
+     * @param ObjectProphecy $plainMailTemplate
+     */
+    protected function mockRecoveryConfigurationAndUserRepository(
+        string $emailAddress,
+        array $recoveryConfiguration,
+        array $userInformation,
+        ObjectProphecy $plainMailTemplate
+    ): void {
+        $this->recoveryConfiguration->getForgotHash()->willReturn($recoveryConfiguration['forgotHash']);
+        $this->recoveryConfiguration->getLifeTimeTimestamp()->willReturn($recoveryConfiguration['lifeTimeTimestamp']);
+        $this->recoveryConfiguration->getPlainMailTemplate()->willReturn($plainMailTemplate->reveal());
+        $this->recoveryConfiguration->getSender()->willReturn($recoveryConfiguration['sender']);
+        $this->recoveryConfiguration->hasHtmlMailTemplate()->willReturn($recoveryConfiguration['hasHtmlMailTemplate']);
+        $this->recoveryConfiguration->getReplyTo()->willReturn($recoveryConfiguration['replyTo']);
+
+        $this->userRepository->updateForgotHashForUserByEmail($emailAddress, $recoveryConfiguration['forgotHash'])
+            ->shouldBeCalledOnce();
+        $this->userRepository->fetchUserInformationByEmail($emailAddress)
+            ->willReturn($userInformation);
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Unit/Service/TreeUidListProviderTest.php b/typo3/sysext/felogin/Tests/Unit/Service/TreeUidListProviderTest.php
new file mode 100644 (file)
index 0000000..c7480d3
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\FrontendLogin\Helper\TreeUidListProvider;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Class TreeUidListProviderTest
+ */
+class TreeUidListProviderTest extends UnitTestCase
+{
+    /**
+     * @var ContentObjectRenderer|ObjectProphecy
+     */
+    protected $cObj;
+    /**
+     * @var TreeUidListProvider
+     */
+    protected $subject;
+
+    /**
+     * @test
+     */
+    public function getListForIdListDirectlyReturnsPassedListWhileDepthIsZero(): void
+    {
+        $uidList = '1,2,3';
+
+        self::assertSame($uidList, $this->subject->getListForIdList($uidList));
+        $this->cObj->getTreeList(Argument::any(), 0)->shouldNotHaveBeenCalled();
+    }
+
+    /**
+     * @test
+     */
+    public function getListForIdListReturnsListOfAllUidListsWithDuplicatedIdsPossible(): void
+    {
+        $uidList = '1,5';
+        $treeLists = ['1,2,3,4,5,6,7', '3,4'];
+        $expected = '1,2,3,4,5,6,7,3,4';
+
+        $this->cObj->getTreeList(Argument::any(), 2)->willReturn(...$treeLists);
+
+        self::assertSame($expected, $this->subject->getListForIdList($uidList, 2, false));
+    }
+
+    /**
+     * @test
+     */
+    public function getListForIdListShouldRemoveDuplicatedIdsFromList(): void
+    {
+        $uidList = '1,5';
+        $treeLists = ['1,2,3,4,5,6,7', '3,4'];
+        $expected = '1,2,3,4,5,6,7';
+
+        $this->cObj->getTreeList(Argument::any(), 2)->willReturn(...$treeLists);
+
+        self::assertSame($expected, $this->subject->getListForIdList($uidList, 2));
+    }
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->cObj = $this->prophesize(ContentObjectRenderer::class);
+        $this->subject = new TreeUidListProvider($this->cObj->reveal());
+    }
+}
diff --git a/typo3/sysext/felogin/Tests/Unit/Service/ValidatorResolverServiceTest.php b/typo3/sysext/felogin/Tests/Unit/Service/ValidatorResolverServiceTest.php
new file mode 100644 (file)
index 0000000..959de68
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Service;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator;
+use TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator;
+use TYPO3\CMS\FrontendLogin\Service\ValidatorResolverService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Class ValidatorResolverServiceTest
+ */
+class ValidatorResolverServiceTest extends UnitTestCase
+{
+    /**
+     * @var ValidatorResolverService
+     */
+    protected $subject;
+
+    /**
+     * @test
+     */
+    public function resolveShouldReturnEmptyArrayIfEmptyConfigurationIsPassed(): void
+    {
+        $result = $this->subject->resolve([]);
+
+        self::assertEmpty($result->current());
+    }
+
+    /**
+     * @test
+     * @dataProvider validatorConfigDataProvider
+     * @param array $config
+     */
+    public function resolveShouldReturnValidators(array $config): void
+    {
+        $validators = $this->subject->resolve($config);
+
+        foreach ($validators as $key => $validator) {
+            $className = is_string($config[$key]) ? $config[$key] : $config[$key]['className'];
+
+            self::assertInstanceOf($className, $validator);
+        }
+    }
+
+    public function validatorConfigDataProvider(): \Generator
+    {
+        return [
+            yield 'simple className' => ['config' => [NotEmptyValidator::class]],
+            yield 'with options' => [
+                'config' => [['className' => StringLengthValidator::class, 'options' => ['minimum' => 3]]],
+            ],
+            yield 'complex with both options and simple class names' => [
+                'config' => [NotEmptyValidator::class, ['className' => StringLengthValidator::class, 'options' => ['minimum' => 3]]],
+            ],
+        ];
+    }
+
+    protected function setUp(): void
+    {
+        $this->subject = new ValidatorResolverService;
+
+        parent::setUp();
+    }
+}
index ecc19f1..2483def 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 declare(strict_types = 1);
 
-namespace TYPO3\CMS\Felogin\Tests\Unit\Validation;
+namespace TYPO3\CMS\FrontendLogin\Tests\Unit\Validation;
 
 /*
  * This file is part of the TYPO3 CMS project.
@@ -20,7 +20,7 @@ use Psr\Log\NullLogger;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\SiteFinder;
-use TYPO3\CMS\Felogin\Validation\RedirectUrlValidator;
+use TYPO3\CMS\FrontendLogin\Validation\RedirectUrlValidator;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
index 4140e90..75988d6 100644 (file)
        },
        "autoload": {
                "psr-4": {
-                       "TYPO3\\CMS\\Felogin\\": "Classes/"
+                       "TYPO3\\CMS\\Felogin\\": "Classes/",
+                       "TYPO3\\CMS\\FrontendLogin\\": "Classes/"
                }
        },
        "autoload-dev": {
                "psr-4": {
-                       "TYPO3\\CMS\\Felogin\\Tests\\": "Tests/"
+                       "TYPO3\\CMS\\Felogin\\Tests\\": "Tests/",
+                       "TYPO3\\CMS\\FrontendLogin\\Tests\\": "Tests/"
                }
        }
 }
index 773ce26..d7fc125 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 defined('TYPO3_MODE') or die();
 
-// Add a default TypoScript for the CType "login"
+// Add default TypoScript
 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptConstants(
     "@import 'EXT:felogin/Configuration/TypoScript/constants.typoscript'"
 );
@@ -9,14 +9,49 @@ defined('TYPO3_MODE') or die();
     "@import 'EXT:felogin/Configuration/TypoScript/setup.typoscript'"
 );
 
-// Add login to new content element wizard
-\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
-    "@import 'EXT:felogin/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig'"
-);
+// Add additional TypoScript & TsConfig depending on the value of the feature toggle "felogin.extbase"
+$feloginExtbase = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\Features::class)
+    ->isFeatureEnabled('felogin.extbase');
+
+if (!$feloginExtbase) {
+    // Add a default TypoScript for the CType "login" with PiBase Plugin
+    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptConstants(
+        "@import 'EXT:felogin/Configuration/TypoScript/PiBase/constants.typoscript'"
+    );
+    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
+        "@import 'EXT:felogin/Configuration/TypoScript/PiBase/setup.typoscript'"
+    );
+
+    // Add login form to new content element wizard
+    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
+        "@import 'EXT:felogin/Configuration/TsConfig/Page/PiBase/Mod/Wizards/NewContentElement.tsconfig'"
+    );
+} else {
+    \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
+        'Felogin',
+        'Login',
+        [
+            \TYPO3\CMS\FrontendLogin\Controller\LoginController::class => 'login, overview',
+            \TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController::class => 'recovery,showChangePassword,changePassword'
+        ],
+        [
+            \TYPO3\CMS\FrontendLogin\Controller\LoginController::class => 'login, overview',
+            \TYPO3\CMS\FrontendLogin\Controller\PasswordRecoveryController::class => 'recovery,showChangePassword,changePassword'
+        ],
+        \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
+    );
+
+    // Add login form to new content element wizard
+    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
+        "@import 'EXT:felogin/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig'"
+    );
+}
 
 // Page module hook
-$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['felogin'] = \TYPO3\CMS\Felogin\Hooks\CmsLayout::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['felogin'] = \TYPO3\CMS\FrontendLogin\Hooks\CmsLayout::class;
 
-// Add migration wizard
+// Add migration wizards
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\TYPO3\CMS\Felogin\Updates\MigrateFeloginPlugins::class]
     = \TYPO3\CMS\Felogin\Updates\MigrateFeloginPlugins::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\TYPO3\CMS\FrontendLogin\Updates\MigrateFeloginPluginsCtype::class]
+    = \TYPO3\CMS\FrontendLogin\Updates\MigrateFeloginPluginsCtype::class;