TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Configuration;
|
||||
|
||||
/**
|
||||
* @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 {}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Configuration;
|
||||
|
||||
use Symfony\Component\Mime\Address;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
|
||||
/**
|
||||
* @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
|
||||
*/
|
||||
class RecoveryConfiguration
|
||||
{
|
||||
protected readonly string $forgotHash;
|
||||
protected ?Address $replyTo = null;
|
||||
protected Address $sender;
|
||||
protected readonly array $settings;
|
||||
protected string $mailTemplateName = '';
|
||||
protected ?int $timestamp = null;
|
||||
|
||||
public function __construct(
|
||||
protected readonly Context $context,
|
||||
ConfigurationManagerInterface $configurationManager,
|
||||
Random $random,
|
||||
HashService $hashService
|
||||
) {
|
||||
$this->settings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
|
||||
$this->forgotHash = $this->getLifeTimeTimestamp() . '|' . $this->generateHash($random, $hashService);
|
||||
$this->resolveFromTypoScript();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the forgot hash.
|
||||
*/
|
||||
public function getForgotHash(): string
|
||||
{
|
||||
return $this->forgotHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns email template name configured in TypoScript
|
||||
*/
|
||||
public function getMailTemplateName(): string
|
||||
{
|
||||
return $this->mailTemplateName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TTL timestamp of the forgot hash
|
||||
*/
|
||||
public function getLifeTimeTimestamp(): int
|
||||
{
|
||||
if ($this->timestamp === null) {
|
||||
$lifetimeInHours = (int)($this->settings['forgotLinkHashValidTime'] ?? 0) ?: 12;
|
||||
$currentTimestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
|
||||
$this->timestamp = $currentTimestamp + 3600 * $lifetimeInHours;
|
||||
}
|
||||
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns reply-to address if configured otherwise null.
|
||||
*/
|
||||
public function getReplyTo(): ?Address
|
||||
{
|
||||
return $this->replyTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sender. Normally the current typo3 installation.
|
||||
*/
|
||||
public function getSender(): Address
|
||||
{
|
||||
return $this->sender;
|
||||
}
|
||||
|
||||
protected function generateHash(Random $random, HashService $hashService): string
|
||||
{
|
||||
$randomString = $random->generateRandomHexString(16);
|
||||
|
||||
return $hashService->hmac($randomString, self::class, HashAlgo::SHA3_256);
|
||||
}
|
||||
|
||||
protected function resolveFromTypoScript(): void
|
||||
{
|
||||
$fromAddress = ($this->settings['email_from'] ?? null) ?: $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'] ?? null) ?: $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->mailTemplateName = (string)($this->settings['email']['templateName'] ?? '');
|
||||
if (empty($this->mailTemplateName)) {
|
||||
throw new IncompleteConfigurationException(
|
||||
'Key "plugin.tx_felogin_login.settings.email.templateName" cannot be empty! Ensure that TypoScript is properly included.',
|
||||
1584998393
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Configuration;
|
||||
|
||||
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.
|
||||
*/
|
||||
class RedirectConfiguration
|
||||
{
|
||||
protected array $modes;
|
||||
|
||||
public function __construct(
|
||||
array|string|null $mode,
|
||||
protected string $firstMode,
|
||||
protected int $pageOnLogin,
|
||||
protected string $domains,
|
||||
protected int $pageOnLoginError,
|
||||
protected int $pageOnLogout
|
||||
) {
|
||||
$this->modes = is_array($mode) ? $mode : GeneralUtility::trimExplode(',', $mode ?? '', true);
|
||||
}
|
||||
|
||||
public function getModes(): array
|
||||
{
|
||||
return $this->modes;
|
||||
}
|
||||
|
||||
public function getFirstMode(): string
|
||||
{
|
||||
return $this->firstMode;
|
||||
}
|
||||
|
||||
public function getPageOnLogin(): int
|
||||
{
|
||||
return $this->pageOnLogin;
|
||||
}
|
||||
|
||||
public function getDomains(): string
|
||||
{
|
||||
return $this->domains;
|
||||
}
|
||||
|
||||
public function getPageOnLoginError(): int
|
||||
{
|
||||
return $this->pageOnLoginError;
|
||||
}
|
||||
|
||||
public function getPageOnLogout(): int
|
||||
{
|
||||
return $this->pageOnLogout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory when creating a configuration out of Extbase / plugin settings.
|
||||
*/
|
||||
public static function fromSettings(array $settings): self
|
||||
{
|
||||
return new RedirectConfiguration(
|
||||
($settings['redirectMode'] ?? ''),
|
||||
(string)($settings['redirectFirstMethod'] ?? ''),
|
||||
(int)($settings['redirectPageLogin'] ?? 0),
|
||||
(string)($settings['domains'] ?? ''),
|
||||
(int)($settings['redirectPageLoginError'] ?? 0),
|
||||
(int)($settings['redirectPageLogout'] ?? 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Authentication\LoginType;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Security\RequestToken;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration;
|
||||
use TYPO3\CMS\FrontendLogin\Event\BeforeRedirectEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Event\LoginConfirmedEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Event\LoginErrorOccurredEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Event\ModifyLoginFormViewEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Redirect\RedirectHandler;
|
||||
|
||||
/**
|
||||
* Used for plugin login
|
||||
*
|
||||
* @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
|
||||
*/
|
||||
class LoginController extends ActionController
|
||||
{
|
||||
public const MESSAGEKEY_DEFAULT = 'welcome';
|
||||
public const MESSAGEKEY_ERROR = 'error';
|
||||
public const MESSAGEKEY_LOGOUT = 'logout';
|
||||
|
||||
protected string $loginType = '';
|
||||
protected string $redirectUrl = '';
|
||||
protected RedirectConfiguration $configuration;
|
||||
|
||||
public function __construct(
|
||||
protected readonly RedirectHandler $redirectHandler,
|
||||
protected readonly Context $context,
|
||||
protected readonly PageRepository $pageRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initialize redirects
|
||||
*/
|
||||
public function initializeAction(): void
|
||||
{
|
||||
$this->loginType = (string)($this->request->getParsedBody()['logintype'] ?? $this->request->getQueryParams()['logintype'] ?? '');
|
||||
$this->configuration = RedirectConfiguration::fromSettings($this->settings);
|
||||
|
||||
if ($this->isLoginOrLogoutInProgress() && !$this->isRedirectDisabled()) {
|
||||
$this->redirectUrl = $this->redirectHandler->processRedirect(
|
||||
$this->request,
|
||||
$this->loginType,
|
||||
$this->configuration,
|
||||
$this->request->hasArgument('redirectReferrer') ? $this->request->getArgument('redirectReferrer') : ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show login form
|
||||
*/
|
||||
public function loginAction(): ResponseInterface
|
||||
{
|
||||
if ($this->isLogoutSuccessful()) {
|
||||
$this->eventDispatcher->dispatch(new LogoutConfirmedEvent($this, $this->view, $this->request));
|
||||
} elseif ($this->hasLoginErrorOccurred()) {
|
||||
$this->eventDispatcher->dispatch(new LoginErrorOccurredEvent($this->request));
|
||||
}
|
||||
|
||||
if (($forwardResponse = $this->handleLoginForwards()) !== null) {
|
||||
return $forwardResponse;
|
||||
}
|
||||
if (($redirectResponse = $this->handleRedirect()) !== null) {
|
||||
return $redirectResponse;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch(new ModifyLoginFormViewEvent($this->view, $this->request));
|
||||
|
||||
$storagePageIds = ($GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] ?? false)
|
||||
? $this->pageRepository->getPageIdsRecursive(GeneralUtility::intExplode(',', (string)($this->settings['pages'] ?? ''), true), (int)($this->settings['recursive'] ?? 0))
|
||||
: [];
|
||||
|
||||
$this->view->assignMultiple(
|
||||
[
|
||||
'messageKey' => $this->getStatusMessageKey(),
|
||||
'permaloginStatus' => $this->getPermaloginStatus(),
|
||||
'redirectURL' => $this->redirectHandler->getLoginFormRedirectUrl($this->request, $this->configuration, $this->isRedirectDisabled()),
|
||||
'redirectReferrer' => $this->request->hasArgument('redirectReferrer') ? (string)$this->request->getArgument('redirectReferrer') : '',
|
||||
'referer' => $this->redirectHandler->getReferrerForLoginForm($this->request, $this->settings),
|
||||
'noRedirect' => $this->isRedirectDisabled(),
|
||||
'requestToken' => RequestToken::create('core/user-auth/fe')
|
||||
->withMergedParams(['pid' => implode(',', $storagePageIds)]),
|
||||
]
|
||||
);
|
||||
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* User overview for logged in users
|
||||
*/
|
||||
public function overviewAction(bool $showLoginMessage = false): ResponseInterface
|
||||
{
|
||||
if (!$this->context->getAspect('frontend.user')->isLoggedIn()) {
|
||||
return new ForwardResponse('login');
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new LoginConfirmedEvent($this, $this->view, $this->request));
|
||||
if (($redirectResponse = $this->handleRedirect()) !== null) {
|
||||
return $redirectResponse;
|
||||
}
|
||||
$this->view->assignMultiple(
|
||||
[
|
||||
'user' => $this->request->getAttribute('frontend.user')->user,
|
||||
'showLoginMessage' => $showLoginMessage,
|
||||
]
|
||||
);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show logout form. Note, that this action should never process any redirects.
|
||||
*/
|
||||
public function logoutAction(): ResponseInterface
|
||||
{
|
||||
$this->view->assignMultiple(
|
||||
[
|
||||
'user' => $this->request->getAttribute('frontend.user')->user,
|
||||
'noRedirect' => $this->isRedirectDisabled(),
|
||||
]
|
||||
);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the redirect when $this->redirectUrl is not empty
|
||||
*/
|
||||
protected function handleRedirect(): ?ResponseInterface
|
||||
{
|
||||
if ($this->redirectUrl !== '') {
|
||||
$event = new BeforeRedirectEvent($this->loginType, $this->redirectUrl, $this->request);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
if ($event->getRedirectUrl() !== '') {
|
||||
return $this->redirectToUri($event->getRedirectUrl());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle forwards to overview and logout actions from login action
|
||||
*/
|
||||
protected function handleLoginForwards(): ?ResponseInterface
|
||||
{
|
||||
if ($this->shouldRedirectToOverview()) {
|
||||
return (new ForwardResponse('overview'))->withArguments(['showLoginMessage' => true]);
|
||||
}
|
||||
if ($this->context->getAspect('frontend.user')->isLoggedIn()) {
|
||||
return new ForwardResponse('logout');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
protected function shouldRedirectToOverview(): bool
|
||||
{
|
||||
return $this->context->getAspect('frontend.user')->isLoggedIn()
|
||||
&& (LoginType::tryFrom($this->loginType) === LoginType::LOGIN)
|
||||
&& !($this->settings['showLogoutFormAfterLogin'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return message key based on user login status
|
||||
*/
|
||||
protected function getStatusMessageKey(): string
|
||||
{
|
||||
$messageKey = self::MESSAGEKEY_DEFAULT;
|
||||
if ($this->hasLoginErrorOccurred()) {
|
||||
$messageKey = self::MESSAGEKEY_ERROR;
|
||||
} elseif (LoginType::tryFrom($this->loginType) === LoginType::LOGOUT) {
|
||||
$messageKey = self::MESSAGEKEY_LOGOUT;
|
||||
}
|
||||
|
||||
return $messageKey;
|
||||
}
|
||||
|
||||
protected function isLoginOrLogoutInProgress(): bool
|
||||
{
|
||||
$type = LoginType::tryFrom($this->loginType);
|
||||
return $type === LoginType::LOGIN || $type === LoginType::LOGOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is redirect disabled by setting or noredirect GET/POST parameter
|
||||
*/
|
||||
protected function isRedirectDisabled(): bool
|
||||
{
|
||||
return
|
||||
(int)($this->request->getParsedBody()['noredirect'] ?? $this->request->getQueryParams()['noredirect'] ?? 0) === 1
|
||||
|| ($this->settings['noredirect'] ?? false)
|
||||
|| ($this->settings['redirectDisable'] ?? false);
|
||||
}
|
||||
|
||||
protected function isLogoutSuccessful(): bool
|
||||
{
|
||||
return LoginType::tryFrom($this->loginType) === LoginType::LOGOUT && !$this->context->getAspect('frontend.user')->isLoggedIn();
|
||||
}
|
||||
|
||||
protected function hasLoginErrorOccurred(): bool
|
||||
{
|
||||
return LoginType::tryFrom($this->loginType) === LoginType::LOGIN && !$this->context->getAspect('frontend.user')->isLoggedIn();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
|
||||
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
|
||||
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;
|
||||
use TYPO3\CMS\Core\Session\SessionManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Error\Error;
|
||||
use TYPO3\CMS\Extbase\Error\Result;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\Extbase\Mvc\Exception\NoSuchArgumentException;
|
||||
use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration;
|
||||
use TYPO3\CMS\FrontendLogin\Domain\Repository\FrontendUserRepository;
|
||||
use TYPO3\CMS\FrontendLogin\Event\PasswordChangeEvent;
|
||||
use TYPO3\CMS\FrontendLogin\Service\RecoveryService;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
public function __construct(
|
||||
protected RecoveryService $recoveryService,
|
||||
protected FrontendUserRepository $userRepository,
|
||||
protected RecoveryConfiguration $recoveryConfiguration,
|
||||
protected readonly Features $features,
|
||||
protected readonly PageRepository $pageRepository,
|
||||
protected RateLimiterFactoryInterface $rateLimiterFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Shows the recovery form. If $userIdentifier is set, an email will be sent, if the corresponding user exists and
|
||||
* has a valid email address set.
|
||||
*/
|
||||
public function recoveryAction(?string $userIdentifier = null): ResponseInterface
|
||||
{
|
||||
if (empty($userIdentifier)) {
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
$storagePageIds = ($GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] ?? false)
|
||||
? $this->pageRepository->getPageIdsRecursive(GeneralUtility::intExplode(',', (string)($this->settings['pages'] ?? ''), true), (int)($this->settings['recursive'] ?? 0))
|
||||
: [];
|
||||
|
||||
$userData = $this->userRepository->findUserByUsernameOrEmailOnPages($userIdentifier, $storagePageIds);
|
||||
|
||||
if ($userData
|
||||
&& GeneralUtility::validEmail($userData['email'])
|
||||
&& !$this->hasExceededMaximumAttemptsForReset($userData['email'])
|
||||
) {
|
||||
$hash = $this->recoveryConfiguration->getForgotHash();
|
||||
$this->userRepository->updateForgotHashForUserByUid($userData['uid'], $this->hashService->hmac($hash, self::class, HashAlgo::SHA3_256));
|
||||
$this->recoveryService->sendRecoveryEmail($this->request, $userData, $hash);
|
||||
}
|
||||
|
||||
// Prevent time based information disclosure by waiting a random time before sending a response. This prevents
|
||||
// that the response time can be an indicator if the used username or email exists or not. Wait a random time
|
||||
// between 200 milliseconds and 3 seconds.
|
||||
usleep(random_int(200000, 3000000));
|
||||
|
||||
// Always show the default message and never notify about a potential rate limit, because this would reveal,
|
||||
// that a given user identifier is actually valid.
|
||||
$this->addFlashMessage($this->getTranslation('forgot_reset_message_emailSent'));
|
||||
|
||||
return $this->redirect('login', 'Login', 'felogin');
|
||||
}
|
||||
|
||||
protected function hasExceededMaximumAttemptsForReset(string $email): bool
|
||||
{
|
||||
$limiter = $this->rateLimiterFactory->create($email);
|
||||
$limit = $limiter->consume();
|
||||
return !$limit->isAccepted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the hash argument and make sure that:
|
||||
*
|
||||
* - it is in the expected format
|
||||
* - it is not expired
|
||||
* - a fe_user with the given hash exists
|
||||
*
|
||||
* If one of the checks fail, a redirect response to the recoveryAction() is returned
|
||||
*/
|
||||
protected function validateHashArgument(): ?ResponseInterface
|
||||
{
|
||||
$hash = $this->request->hasArgument('hash') ? $this->request->getArgument('hash') : '';
|
||||
$hash = is_string($hash) ? $hash : '';
|
||||
|
||||
if (!$this->validateHashFormat($hash)) {
|
||||
return $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($this->hashService->hmac($hash, self::class, HashAlgo::SHA3_256))) {
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = clone $this->request->getAttribute('extbase');
|
||||
$originalResult = $extbaseRequestParameters->getOriginalRequestMappingResults();
|
||||
$originalResult->addError(new Error($this->getTranslation('change_password_notvalid_message'), 1554994253));
|
||||
$extbaseRequestParameters->setOriginalRequestMappingResults($originalResult);
|
||||
$this->request = $this->request->withAttribute('extbase', $extbaseRequestParameters);
|
||||
|
||||
return (new ForwardResponse('recovery'))
|
||||
->withControllerName('PasswordRecovery')
|
||||
->withExtensionName('felogin')
|
||||
->withArgumentsValidationResult($originalResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the change password form if a valid hash is available.
|
||||
*/
|
||||
public function showChangePasswordAction(string $hash = ''): ResponseInterface
|
||||
{
|
||||
// Validate hash (lifetime, format and fe_user with hash persistence)
|
||||
if (($response = $this->validateHashArgument()) instanceof ResponseInterface) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'hash' => $hash,
|
||||
'passwordRequirements' => $this->getPasswordPolicyValidator()->getRequirements(),
|
||||
]);
|
||||
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the hash argument, the entered password and passwordRepeat values. If one of the values is considered
|
||||
* as invalid, a response object with validation errors in the mapping results is returned.
|
||||
*
|
||||
* @throws NoSuchArgumentException
|
||||
*/
|
||||
public function validateHashAndPasswords()
|
||||
{
|
||||
// Validate hash (lifetime, format and fe_user with hash persistence)
|
||||
if (($response = $this->validateHashArgument()) instanceof ResponseInterface) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Exit early if newPass or newPassRepeat is not set.
|
||||
/** @var ExtbaseRequestParameters $extbaseRequestParameters */
|
||||
$extbaseRequestParameters = clone $this->request->getAttribute('extbase');
|
||||
$originalResult = $extbaseRequestParameters->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
|
||||
));
|
||||
|
||||
return (new ForwardResponse('showChangePassword'))
|
||||
->withControllerName('PasswordRecovery')
|
||||
->withExtensionName('felogin')
|
||||
->withArguments(['hash' => $this->request->getArgument('hash')])
|
||||
->withArgumentsValidationResult($originalResult);
|
||||
}
|
||||
|
||||
$this->validateNewPassword($originalResult);
|
||||
|
||||
// if an error exists, forward with all messages to the change password form
|
||||
if ($originalResult->hasErrors()) {
|
||||
return (new ForwardResponse('showChangePassword'))
|
||||
->withControllerName('PasswordRecovery')
|
||||
->withExtensionName('felogin')
|
||||
->withArguments(['hash' => $this->request->getArgument('hash')])
|
||||
->withArgumentsValidationResult($originalResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change actual password. Hash $newPass and update the user with the corresponding $hash.
|
||||
*
|
||||
* @throws AspectNotFoundException
|
||||
* @throws InvalidPasswordHashException
|
||||
*/
|
||||
public function changePasswordAction(string $newPass, string $hash): ResponseInterface
|
||||
{
|
||||
if (($response = $this->validateHashAndPasswords()) instanceof ResponseInterface) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$hashedPassword = GeneralUtility::makeInstance(PasswordHashFactory::class)
|
||||
->getDefaultHashInstance('FE')
|
||||
->getHashedPassword($newPass);
|
||||
|
||||
$hmac = $this->hashService->hmac($hash, self::class, HashAlgo::SHA3_256);
|
||||
$user = $this->userRepository->findOneByForgotPasswordHash($hmac);
|
||||
|
||||
$event = new PasswordChangeEvent($user, $hashedPassword, $newPass, $this->request);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
$this->userRepository->updatePasswordAndInvalidateHash($hmac, $hashedPassword);
|
||||
$this->invalidateUserSessions($user['uid']);
|
||||
|
||||
$this->addFlashMessage($this->getTranslation('change_password_done_message'));
|
||||
|
||||
return $this->redirect('login', 'Login', 'felogin', ['redirectReferrer' => 'off']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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));
|
||||
}
|
||||
|
||||
$hash = $this->request->getArgument('hash');
|
||||
$userData = $this->userRepository->findOneByForgotPasswordHash($this->hashService->hmac($hash, self::class, HashAlgo::SHA3_256));
|
||||
|
||||
// Validate against password policy
|
||||
$passwordPolicyValidator = $this->getPasswordPolicyValidator();
|
||||
$contextData = new ContextData(
|
||||
loginMode: 'FE',
|
||||
currentPasswordHash: $userData['password']
|
||||
);
|
||||
$contextData->setData('currentUsername', $userData['username']);
|
||||
$contextData->setData('currentFirstname', $userData['first_name']);
|
||||
$contextData->setData('currentLastname', $userData['last_name']);
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new EnrichPasswordValidationContextDataEvent(
|
||||
$contextData,
|
||||
$userData,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
$contextData = $event->getContextData();
|
||||
|
||||
if (!$passwordPolicyValidator->isValidPassword($newPass, $contextData)) {
|
||||
foreach ($passwordPolicyValidator->getValidationErrors() as $validationError) {
|
||||
$validationResult = new Result();
|
||||
$validationResult->addError(new Error($validationError, 1667647475));
|
||||
$originalResult->merge($validationResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to mock LocalizationUtility::translate
|
||||
*/
|
||||
protected function getTranslation(string $key): string
|
||||
{
|
||||
return (string)LocalizationUtility::translate($key, 'felogin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that $hash is in the expected format (timestamp|forgot_hash)
|
||||
*/
|
||||
protected function validateHashFormat(string $hash): bool
|
||||
{
|
||||
return !empty($hash) && strpos($hash, '|') === 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all frontend user sessions by given user id
|
||||
*/
|
||||
protected function invalidateUserSessions(int $userId): void
|
||||
{
|
||||
$sessionManager = GeneralUtility::makeInstance(SessionManager::class);
|
||||
$sessionBackend = $sessionManager->getSessionBackend('FE');
|
||||
$sessionManager->invalidateAllSessionsByUserId($sessionBackend, $userId);
|
||||
}
|
||||
|
||||
protected function getPasswordPolicyValidator(): PasswordPolicyValidator
|
||||
{
|
||||
$passwordPolicy = $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] ?? 'default';
|
||||
return GeneralUtility::makeInstance(
|
||||
PasswordPolicyValidator::class,
|
||||
PasswordPolicyAction::UPDATE_USER_PASSWORD,
|
||||
is_string($passwordPolicy) ? $passwordPolicy : ''
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Domain\Repository;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
|
||||
/**
|
||||
* @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
|
||||
*/
|
||||
class FrontendUserGroupRepository
|
||||
{
|
||||
protected readonly Connection $connection;
|
||||
|
||||
public function __construct(ConnectionPool $connectionPool)
|
||||
{
|
||||
$this->connection = $connectionPool->getConnectionForTable('fe_groups');
|
||||
}
|
||||
|
||||
public function findRedirectPageIdByGroupId(int $groupId): ?int
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$query = $queryBuilder
|
||||
->select('felogin_redirectPid')
|
||||
->from('fe_groups')
|
||||
->where(
|
||||
$queryBuilder->expr()->neq('felogin_redirectPid', $this->connection->quote('')),
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($groupId, Connection::PARAM_INT))
|
||||
)
|
||||
->setMaxResults(1);
|
||||
$column = $query->executeQuery()->fetchOne();
|
||||
return $column === false ? null : (int)$column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Domain\Repository;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
|
||||
/**
|
||||
* @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
|
||||
*/
|
||||
readonly class FrontendUserRepository
|
||||
{
|
||||
protected Connection $connection;
|
||||
|
||||
public function __construct(
|
||||
protected Context $context,
|
||||
ConnectionPool $connectionPool,
|
||||
) {
|
||||
$this->connection = $connectionPool->getConnectionForTable('fe_users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password for a user based on forgot password hash.
|
||||
*/
|
||||
public function updatePasswordAndInvalidateHash(string $forgotPasswordHash, string $hashedPassword): void
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$currentTimestamp = $this->context->getPropertyFromAspect('date', 'timestamp');
|
||||
$query = $queryBuilder
|
||||
->update('fe_users')
|
||||
->set('password', $hashedPassword)
|
||||
->set('felogin_forgotHash', $this->connection->quote(''), false)
|
||||
->set('tstamp', $currentTimestamp)
|
||||
->where($queryBuilder->expr()->eq('felogin_forgotHash', $queryBuilder->createNamedParameter($forgotPasswordHash)));
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a user exists with hash as `felogin_forgothash`, otherwise false.
|
||||
*/
|
||||
public function existsUserWithHash(string $hash): bool
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$query = $queryBuilder
|
||||
->count('uid')
|
||||
->from('fe_users')
|
||||
->where($queryBuilder->expr()->eq('felogin_forgotHash', $queryBuilder->createNamedParameter($hash)));
|
||||
return (bool)$query->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets forgot hash for passed user uid.
|
||||
*/
|
||||
public function updateForgotHashForUserByUid(int $uid, string $hash): void
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$query = $queryBuilder
|
||||
->update('fe_users')
|
||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, $this->connection::PARAM_INT)))
|
||||
->set('felogin_forgotHash', $hash);
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an array with all columns (except the password) from the fe_users table for the given username or
|
||||
* email on the given pages. Returns null, if user was not found or if user has no email address set.
|
||||
*/
|
||||
public function findUserByUsernameOrEmailOnPages(string $usernameOrEmail, array $pages = []): ?array
|
||||
{
|
||||
if ($usernameOrEmail === '') {
|
||||
return null;
|
||||
}
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$query = $queryBuilder
|
||||
->select('*')
|
||||
->from('fe_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($usernameOrEmail)),
|
||||
$queryBuilder->expr()->eq('email', $queryBuilder->createNamedParameter($usernameOrEmail)),
|
||||
),
|
||||
$queryBuilder->expr()->neq('email', $this->connection->quote('')),
|
||||
);
|
||||
if (!empty($pages)) {
|
||||
// respect storage pid
|
||||
$query->andWhere($queryBuilder->expr()->in('pid', $pages));
|
||||
}
|
||||
$result = $query->executeQuery()->fetchAssociative() ?: null;
|
||||
if ($result) {
|
||||
unset($result['password']);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function findOneByForgotPasswordHash(string $hash): ?array
|
||||
{
|
||||
if ($hash === '') {
|
||||
return null;
|
||||
}
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$query = $queryBuilder
|
||||
->select('*')
|
||||
->from('fe_users')
|
||||
->where($queryBuilder->expr()->eq('felogin_forgotHash', $queryBuilder->createNamedParameter($hash)))
|
||||
->setMaxResults(1);
|
||||
$row = $query->executeQuery()->fetchAssociative();
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
public function findRedirectIdPageByUserId(int $uid): ?int
|
||||
{
|
||||
$queryBuilder = $this->connection->createQueryBuilder();
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$query = $queryBuilder
|
||||
->select('felogin_redirectPid')
|
||||
->from('fe_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->neq('felogin_redirectPid', $this->connection->quote('')),
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))
|
||||
)
|
||||
->setMaxResults(1);
|
||||
$column = $query->executeQuery()->fetchOne();
|
||||
return $column === false ? null : (int)$column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\FrontendLogin\Controller\LoginController;
|
||||
|
||||
/**
|
||||
* A confirmation notification when an login/logout action has successfully arrived at the plugin, via the view and the controller, multiple
|
||||
* information can be overridden in Event Listeners.
|
||||
*/
|
||||
abstract class AbstractConfirmedEvent
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly LoginController $controller,
|
||||
private ViewInterface $view,
|
||||
protected readonly ServerRequestInterface $request
|
||||
) {}
|
||||
|
||||
public function getController(): LoginController
|
||||
{
|
||||
return $this->controller;
|
||||
}
|
||||
|
||||
public function getView(): ViewInterface
|
||||
{
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Notification before a redirect is made, which also allows to modify
|
||||
* the actual redirect URL. Setting the redirect to an empty string
|
||||
* will avoid triggering a redirect.
|
||||
*/
|
||||
final class BeforeRedirectEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $loginType,
|
||||
private string $redirectUrl,
|
||||
private readonly ServerRequestInterface $request,
|
||||
) {}
|
||||
|
||||
public function getLoginType(): string
|
||||
{
|
||||
return $this->loginType;
|
||||
}
|
||||
|
||||
public function getRedirectUrl(): string
|
||||
{
|
||||
return $this->redirectUrl;
|
||||
}
|
||||
|
||||
public function setRedirectUrl(string $redirectUrl): void
|
||||
{
|
||||
$this->redirectUrl = $redirectUrl;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
/**
|
||||
* 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 extends AbstractConfirmedEvent {}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* A notification if something went wrong while trying to log in a user.
|
||||
*/
|
||||
final readonly class LoginErrorOccurredEvent
|
||||
{
|
||||
public function __construct(
|
||||
private ServerRequestInterface $request
|
||||
) {}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
/**
|
||||
* A notification when a log out has successfully arrived at the plugin, via the view and the controller, multiple
|
||||
* information can be overridden in Event Listeners.
|
||||
*/
|
||||
final class LogoutConfirmedEvent extends AbstractConfirmedEvent {}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
|
||||
/**
|
||||
* Allows to inject custom variables into the login form.
|
||||
*/
|
||||
final readonly class ModifyLoginFormViewEvent
|
||||
{
|
||||
public function __construct(
|
||||
private ViewInterface $view,
|
||||
private ServerRequestInterface $request
|
||||
) {}
|
||||
|
||||
public function getView(): ViewInterface
|
||||
{
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Allows to modify the result of the redirect URL validation (e.g. allow redirect to specific external URLs).
|
||||
*/
|
||||
final class ModifyRedirectUrlValidationResultEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $redirectUrl,
|
||||
private bool $validationResult,
|
||||
private readonly ServerRequestInterface $request,
|
||||
) {}
|
||||
|
||||
public function getRedirectUrl(): string
|
||||
{
|
||||
return $this->redirectUrl;
|
||||
}
|
||||
|
||||
public function getValidationResult(): bool
|
||||
{
|
||||
return $this->validationResult;
|
||||
}
|
||||
|
||||
public function setValidationResult(bool $validationResult): void
|
||||
{
|
||||
$this->validationResult = $validationResult;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Informal event that contains information about the password which was set, and is about to be stored in the database.
|
||||
*/
|
||||
final readonly class PasswordChangeEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $user,
|
||||
private string $passwordHash,
|
||||
private string $rawPassword,
|
||||
private ServerRequestInterface $request
|
||||
) {}
|
||||
|
||||
public function getUser(): array
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getHashedPassword(): string
|
||||
{
|
||||
return $this->passwordHash;
|
||||
}
|
||||
|
||||
public function getRawPassword(): string
|
||||
{
|
||||
return $this->rawPassword;
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Authentication\Event\BeforeRequestTokenProcessedEvent;
|
||||
use TYPO3\CMS\Core\Security\RequestToken;
|
||||
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
|
||||
|
||||
/**
|
||||
* Process request token.
|
||||
*/
|
||||
final class ProcessRequestTokenListener
|
||||
{
|
||||
#[AsEventListener('felogin-process-request-token')]
|
||||
public function __invoke(BeforeRequestTokenProcessedEvent $event): void
|
||||
{
|
||||
$user = $event->getUser();
|
||||
$requestToken = $event->getRequestToken();
|
||||
if (!$user instanceof FrontendUserAuthentication || !$requestToken instanceof RequestToken) {
|
||||
return;
|
||||
}
|
||||
$pidParam = (string)($requestToken->params['pid'] ?? '');
|
||||
if ($user->checkPid) {
|
||||
$user->checkPid_value = $pidParam;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Mail\FluidEmail;
|
||||
|
||||
/**
|
||||
* Event that contains the email to be sent to the user when they request a new password.
|
||||
* More
|
||||
*
|
||||
* Additional validation can happen here.
|
||||
*/
|
||||
final readonly class SendRecoveryEmailEvent
|
||||
{
|
||||
public function __construct(private FluidEmail $email, private array $user) {}
|
||||
|
||||
public function getUserInformation(): array
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getEmail(): FluidEmail
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Redirect;
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\LoginType;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\FrontendLogin\Configuration\RedirectConfiguration;
|
||||
use TYPO3\CMS\FrontendLogin\Validation\RedirectUrlValidator;
|
||||
|
||||
/**
|
||||
* Resolve felogin related redirects based on the current login type and the selected configuration (redirect mode)
|
||||
*
|
||||
* @internal this is a concrete TYPO3 implementation and solely used for EXT:felogin and not part of TYPO3's Core API.
|
||||
*/
|
||||
class RedirectHandler
|
||||
{
|
||||
protected bool $userIsLoggedIn = false;
|
||||
|
||||
public function __construct(
|
||||
protected RedirectModeHandler $redirectModeHandler,
|
||||
protected RedirectUrlValidator $redirectUrlValidator,
|
||||
Context $context
|
||||
) {
|
||||
$this->userIsLoggedIn = (bool)$context->getPropertyFromAspect('frontend.user', 'isLoggedIn');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process redirect modes. This method searches for a redirect url using all configured modes and returns it.
|
||||
*/
|
||||
public function processRedirect(RequestInterface $request, string $loginType, RedirectConfiguration $configuration, string $redirectModeReferrer): string
|
||||
{
|
||||
if ($this->isUserLoginFailedAndLoginErrorActive($configuration->getModes(), $loginType)) {
|
||||
return $this->redirectModeHandler->redirectModeLoginError($request, $configuration->getPageOnLoginError());
|
||||
}
|
||||
|
||||
$redirectUrlList = [];
|
||||
foreach ($configuration->getModes() as $redirectMode) {
|
||||
$redirectUrl = '';
|
||||
|
||||
$type = LoginType::tryFrom($loginType);
|
||||
if ($type === LoginType::LOGIN) {
|
||||
$redirectUrl = $this->handleSuccessfulLogin($request, $redirectMode, $configuration->getPageOnLogin(), $configuration->getDomains(), $redirectModeReferrer);
|
||||
} elseif ($type === LoginType::LOGOUT) {
|
||||
$redirectUrl = $this->handleSuccessfulLogout($request, $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
|
||||
*/
|
||||
protected function getLogoutRedirectUrl(RequestInterface $request, array $redirectModes, int $redirectPageLogout = 0): string
|
||||
{
|
||||
if ($this->userIsLoggedIn && $this->isRedirectModeActive($redirectModes, RedirectMode::LOGOUT)) {
|
||||
return $this->redirectModeHandler->redirectModeLogout($request, $redirectPageLogout);
|
||||
}
|
||||
return $this->getGetpostRedirectUrl($request, $redirectModes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is used for alternative redirect urls on redirect mode "getpost"
|
||||
*/
|
||||
protected function getGetpostRedirectUrl(RequestInterface $request, array $redirectModes): string
|
||||
{
|
||||
return $this->isRedirectModeActive($redirectModes, RedirectMode::GETPOST)
|
||||
? $this->getRedirectUrlRequestParam($request)
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode logout
|
||||
*/
|
||||
protected function handleSuccessfulLogout(RequestInterface $request, string $redirectMode, int $redirectPageLogout): string
|
||||
{
|
||||
if ($redirectMode === RedirectMode::LOGOUT) {
|
||||
return $this->redirectModeHandler->redirectModeLogout($request, $redirectPageLogout);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Base on setting redirectFirstMethod get first or last entry from redirect url list.
|
||||
*/
|
||||
protected function fetchReturnUrlFromList(array $redirectUrlList, string $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, static function (string $value): bool {
|
||||
return strlen($value) > 0;
|
||||
});
|
||||
|
||||
return $redirectFirstMethod
|
||||
? array_shift($redirectUrlList)
|
||||
: array_pop($redirectUrlList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate redirect_url for case that the user was successfully logged in
|
||||
*/
|
||||
protected function handleSuccessfulLogin(RequestInterface $request, 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($request);
|
||||
break;
|
||||
case RedirectMode::USER_LOGIN:
|
||||
$redirectUrl = $this->redirectModeHandler->redirectModeUserLogin($request);
|
||||
break;
|
||||
case RedirectMode::LOGIN:
|
||||
$redirectUrl = $this->redirectModeHandler->redirectModeLogin($request, $redirectPageLogin);
|
||||
break;
|
||||
case RedirectMode::GETPOST:
|
||||
$redirectUrl = $this->getRedirectUrlRequestParam($request);
|
||||
break;
|
||||
case RedirectMode::REFERRER:
|
||||
$redirectUrl = $this->redirectModeHandler->redirectModeReferrer($request, $redirectModeReferrer);
|
||||
break;
|
||||
case RedirectMode::REFERRER_DOMAINS:
|
||||
$redirectUrl = $this->redirectModeHandler->redirectModeReferrerDomains($request, $domains, $redirectModeReferrer);
|
||||
break;
|
||||
default:
|
||||
$redirectUrl = '';
|
||||
}
|
||||
|
||||
return $redirectUrl;
|
||||
}
|
||||
|
||||
protected function isUserLoginFailedAndLoginErrorActive(array $redirectModes, string $loginType): bool
|
||||
{
|
||||
return LoginType::tryFrom($loginType) === LoginType::LOGIN
|
||||
&& !$this->userIsLoggedIn
|
||||
&& $this->isRedirectModeActive($redirectModes, RedirectMode::LOGIN_ERROR);
|
||||
}
|
||||
|
||||
protected function isRedirectModeActive(array $redirectModes, string $mode): bool
|
||||
{
|
||||
return in_array($mode, $redirectModes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the redirect Url that should be used in login form template for GET/POST redirect mode
|
||||
*/
|
||||
public function getLoginFormRedirectUrl(
|
||||
RequestInterface $request,
|
||||
RedirectConfiguration $configuration,
|
||||
bool $redirectDisabled
|
||||
): string {
|
||||
if (!$redirectDisabled) {
|
||||
return $this->getGetpostRedirectUrl($request, $configuration->getModes());
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the `referer` variable used in the login form for loginMode=referer depending on the
|
||||
* following evaluation order:
|
||||
*
|
||||
* - HTTP POST parameter `referer`
|
||||
* - HTTP GET parameter `referer`
|
||||
* - HTTP_REFERER
|
||||
* - URL of initiating request in case plugin has been called via sub-request
|
||||
*
|
||||
* The evaluated `referer` is only returned, if it is considered valid.
|
||||
*/
|
||||
public function getReferrerForLoginForm(RequestInterface $request, array $settings): string
|
||||
{
|
||||
// Early return, if redirectMode is not configured to respect the referrer
|
||||
if (!$this->isReferrerRedirectEnabled($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Early return, if redirectReferrer is not enabled in current context (e.g., after a password reset)
|
||||
if (($request->getQueryParams()['tx_felogin_login']['redirectReferrer'] ?? '') === 'off') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$referrer = (string)(
|
||||
$request->getParsedBody()['referer']
|
||||
?? $request->getQueryParams()['referer']
|
||||
?? $request->getServerParams()['HTTP_REFERER']
|
||||
?? ''
|
||||
);
|
||||
|
||||
// If the current request was initiated via sub-request, we use the URI of the original request as referrer
|
||||
if ($originalRequest = $request->getAttribute('originalRequest', false)) {
|
||||
$referrer = (string)$originalRequest->getUri();
|
||||
}
|
||||
|
||||
if ($this->redirectUrlValidator->isValid($request, $referrer)) {
|
||||
return $referrer;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether redirect based on the referrer is enabled
|
||||
*/
|
||||
protected function isReferrerRedirectEnabled(array $settings): bool
|
||||
{
|
||||
$referrerRedirectModes = [RedirectMode::REFERRER, RedirectMode::REFERRER_DOMAINS];
|
||||
$configuredRedirectModes = GeneralUtility::trimExplode(',', $settings['redirectMode'] ?? '');
|
||||
return count(array_intersect($configuredRedirectModes, $referrerRedirectModes)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns validated redirect url contained in request param return_url or redirect_url
|
||||
*/
|
||||
private function getRedirectUrlRequestParam(RequestInterface $request): string
|
||||
{
|
||||
// If config.typolinkLinkAccessRestrictedPages is set, the var is return_url
|
||||
$returnUrlFromRequest = (string)($request->getParsedBody()['return_url'] ?? $request->getQueryParams()['return_url'] ?? null);
|
||||
$redirectUrlFromRequest = (string)($request->getParsedBody()['redirect_url'] ?? $request->getQueryParams()['redirect_url'] ?? null);
|
||||
$redirectUrl = $returnUrlFromRequest ?: $redirectUrlFromRequest;
|
||||
|
||||
return $this->redirectUrlValidator->isValid($request, $redirectUrl) ? $redirectUrl : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Redirect;
|
||||
|
||||
/**
|
||||
* 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 string LOGIN = 'login';
|
||||
public const string LOGOUT = 'logout';
|
||||
public const string LOGIN_ERROR = 'loginError';
|
||||
public const string GETPOST = 'getpost';
|
||||
public const string USER_LOGIN = 'userLogin';
|
||||
public const string GROUP_LOGIN = 'groupLogin';
|
||||
public const string REFERRER = 'referer';
|
||||
public const string REFERRER_DOMAINS = 'refererDomains';
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Redirect;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
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\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.
|
||||
*/
|
||||
readonly class RedirectModeHandler
|
||||
{
|
||||
public function __construct(
|
||||
protected UriBuilder $uriBuilder,
|
||||
protected RedirectUrlValidator $redirectUrlValidator,
|
||||
private FrontendUserRepository $frontendUserRepository,
|
||||
private FrontendUserGroupRepository $frontendUserGroupRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle redirect mode groupLogin
|
||||
*/
|
||||
public function redirectModeGroupLogin(RequestInterface $request): string
|
||||
{
|
||||
$groups = $request->getAttribute('frontend.user')->userGroups;
|
||||
if (empty($groups)) {
|
||||
return '';
|
||||
}
|
||||
$groupUids = array_keys($groups);
|
||||
// Take the first group with a redirect page
|
||||
foreach ($groupUids as $groupUid) {
|
||||
$redirectPageId = (int)$this->frontendUserGroupRepository
|
||||
->findRedirectPageIdByGroupId($groupUid);
|
||||
if ($redirectPageId > 0) {
|
||||
return $this->buildUriForPageUid($request, $redirectPageId);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode userLogin
|
||||
*/
|
||||
public function redirectModeUserLogin(RequestInterface $request): string
|
||||
{
|
||||
$userUid = (int)$request->getAttribute('frontend.user')->user['uid'];
|
||||
$redirectPageId = $this->frontendUserRepository->findRedirectIdPageByUserId($userUid);
|
||||
if ($redirectPageId === null) {
|
||||
return '';
|
||||
}
|
||||
return $this->buildUriForPageUid($request, $redirectPageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode login
|
||||
*/
|
||||
public function redirectModeLogin(RequestInterface $request, int $redirectPageLogin): string
|
||||
{
|
||||
$redirectUrl = '';
|
||||
if ($redirectPageLogin !== 0) {
|
||||
$redirectUrl = $this->buildUriForPageUid($request, $redirectPageLogin);
|
||||
}
|
||||
return $redirectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode referrer
|
||||
*/
|
||||
public function redirectModeReferrer(RequestInterface $request, string $redirectReferrer): string
|
||||
{
|
||||
$redirectUrl = '';
|
||||
if ($redirectReferrer !== 'off') {
|
||||
// Avoid forced logout, when trying to login immediately after a logout
|
||||
$redirectUrl = preg_replace('/[&?]logintype=[a-z]+/', '', $this->getReferrer($request));
|
||||
}
|
||||
return $redirectUrl ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode refererDomains
|
||||
*/
|
||||
public function redirectModeReferrerDomains(RequestInterface $request, string $domains, string $redirectReferrer): string
|
||||
{
|
||||
$redirectUrl = '';
|
||||
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
|
||||
// also avoid redirect when logging in after changing password
|
||||
if ($domains) {
|
||||
$url = $this->getReferrer($request);
|
||||
// Is referring url allowed to redirect?
|
||||
$match = [];
|
||||
if (preg_match('#^https?://([[:alnum:].-]+)/#', $url, $match)) {
|
||||
$redirectDomain = $match[1];
|
||||
$found = false;
|
||||
foreach (GeneralUtility::trimExplode(',', $domains, true) as $domain) {
|
||||
if (preg_match('/(?:^|\\.)' . preg_quote($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
|
||||
*/
|
||||
public function redirectModeLoginError(RequestInterface $request, int $redirectPageLoginError = 0): string
|
||||
{
|
||||
$redirectUrl = '';
|
||||
if ($redirectPageLoginError > 0) {
|
||||
$redirectUrl = $this->buildUriForPageUid($request, $redirectPageLoginError);
|
||||
}
|
||||
return $redirectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirect mode logout
|
||||
*/
|
||||
public function redirectModeLogout(RequestInterface $request, int $redirectPageLogout): string
|
||||
{
|
||||
$redirectUrl = '';
|
||||
if ($redirectPageLogout > 0) {
|
||||
$redirectUrl = $this->buildUriForPageUid($request, $redirectPageLogout);
|
||||
}
|
||||
return $redirectUrl;
|
||||
}
|
||||
|
||||
protected function buildUriForPageUid(RequestInterface $request, int $pageUid): string
|
||||
{
|
||||
$this->uriBuilder->reset();
|
||||
$this->uriBuilder->setRequest($request);
|
||||
$this->uriBuilder->setTargetPageUid($pageUid);
|
||||
return $this->uriBuilder->build();
|
||||
}
|
||||
|
||||
protected function getReferrer(RequestInterface $request): string
|
||||
{
|
||||
$referrer = '';
|
||||
$requestReferrer = (string)($request->getParsedBody()['referer'] ?? $request->getQueryParams()['referer'] ?? '');
|
||||
if ($this->redirectUrlValidator->isValid($request, $requestReferrer)) {
|
||||
$referrer = $requestReferrer;
|
||||
}
|
||||
return $referrer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Service;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use TYPO3\CMS\Core\Mail\FluidEmail;
|
||||
use TYPO3\CMS\Core\Mail\MailerInterface;
|
||||
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
|
||||
use TYPO3\CMS\FrontendLogin\Configuration\RecoveryConfiguration;
|
||||
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
|
||||
{
|
||||
protected array $settings;
|
||||
|
||||
public function __construct(
|
||||
protected readonly MailerInterface $mailer,
|
||||
protected readonly TemplatedEmailFactory $templatedEmailFactory,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
ConfigurationManagerInterface $configurationManager,
|
||||
protected RecoveryConfiguration $recoveryConfiguration,
|
||||
protected UriBuilder $uriBuilder
|
||||
) {
|
||||
$this->settings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with an absolute link including the given forgot hash to the passed user
|
||||
* with instructions to recover the account.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendRecoveryEmail(RequestInterface $request, array $userData, string $hash): void
|
||||
{
|
||||
$receiver = new Address($userData['email'], $this->getReceiverName($userData));
|
||||
$email = $this->prepareMail($request, $receiver, $hash, $userData);
|
||||
|
||||
$event = new SendRecoveryEmailEvent($email, $userData);
|
||||
$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.
|
||||
*/
|
||||
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 ? $displayName . ' (' . $userInformation['username'] . ')' : $userInformation['username'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create email object from configuration.
|
||||
*/
|
||||
protected function prepareMail(RequestInterface $request, Address $receiver, string $hash, array $userData): FluidEmail
|
||||
{
|
||||
$url = $this->uriBuilder
|
||||
->reset()
|
||||
->setRequest($request)
|
||||
->setCreateAbsoluteUri(true)
|
||||
->uriFor(
|
||||
'showChangePassword',
|
||||
['hash' => $hash],
|
||||
'PasswordRecovery',
|
||||
'felogin',
|
||||
'Login'
|
||||
);
|
||||
|
||||
$variables = [
|
||||
'receiverName' => $receiver->getName(),
|
||||
'userData' => $userData,
|
||||
'url' => $url,
|
||||
'validUntil' => date($this->settings['dateFormat'] ?? 'Y-m-d H:i', $this->recoveryConfiguration->getLifeTimeTimestamp()),
|
||||
];
|
||||
|
||||
$mail = $this->templatedEmailFactory->createWithOverrides(
|
||||
$this->settings['email']['templateRootPaths'] ?? [],
|
||||
$this->settings['email']['layoutRootPaths'] ?? [],
|
||||
$this->settings['email']['partialRootPaths'] ?? [],
|
||||
$request,
|
||||
);
|
||||
$mail->subject($this->getEmailSubject())
|
||||
->from($this->recoveryConfiguration->getSender())
|
||||
->to($receiver)
|
||||
->assignMultiple($variables)
|
||||
->setTemplate($this->recoveryConfiguration->getMailTemplateName());
|
||||
|
||||
$replyTo = $this->recoveryConfiguration->getReplyTo();
|
||||
if ($replyTo) {
|
||||
$mail->addReplyTo($replyTo);
|
||||
}
|
||||
|
||||
return $mail;
|
||||
}
|
||||
|
||||
protected function getEmailSubject(): string
|
||||
{
|
||||
return LocalizationUtility::translate('password_recovery_mail_header', 'felogin');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\FrontendLogin\Validation;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
readonly class RedirectUrlValidator
|
||||
{
|
||||
public function __construct(
|
||||
protected SiteFinder $siteFinder,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks if a given URL is valid / properly sanitized and/or the domain is known to TYPO3.
|
||||
*/
|
||||
public function isValid(RequestInterface $request, string $value): bool
|
||||
{
|
||||
if ($value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate the URL
|
||||
$result = false;
|
||||
if ($this->isRelativeUrl($request, $value) || $this->isInCurrentDomain($request, $value) || $this->isInLocalDomain($value)) {
|
||||
$result = true;
|
||||
}
|
||||
|
||||
// Allow to change the validation result via a PSR-14 event
|
||||
$event = new ModifyRedirectUrlValidationResultEvent($value, $result, $request);
|
||||
$event = $this->eventDispatcher->dispatch($event);
|
||||
$result = $event->getValidationResult();
|
||||
|
||||
// URL is not allowed
|
||||
if (!$result) {
|
||||
$this->logger->debug('Url "{url}" was not accepted.', ['url' => $value]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the URL is on the current host and belongs to the
|
||||
* current TYPO3 installation. The scheme part is ignored in the comparison.
|
||||
*/
|
||||
protected function isInCurrentDomain(RequestInterface $request, string $url): bool
|
||||
{
|
||||
$urlWithoutSchema = preg_replace('#^https?://#', '', $url) ?? '';
|
||||
$siteUrlWithoutSchema = preg_replace('#^https?://#', '', $request->getAttribute('normalizedParams')->getSiteUrl()) ?? '';
|
||||
// this condition only exists to satisfy phpstan, which complains that this could be an array, too.
|
||||
if (is_array($siteUrlWithoutSchema)) {
|
||||
$siteUrlWithoutSchema = $siteUrlWithoutSchema[0];
|
||||
}
|
||||
return str_starts_with($urlWithoutSchema . '/', $request->getAttribute('normalizedParams')->getHttpHost() . '/')
|
||||
&& str_starts_with($urlWithoutSchema, $siteUrlWithoutSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the URL matches a domain known to TYPO3.
|
||||
*/
|
||||
protected function isInLocalDomain(string $url): bool
|
||||
{
|
||||
if (!GeneralUtility::isValidUrl($url)) {
|
||||
return false;
|
||||
}
|
||||
$parsedUrl = parse_url($url);
|
||||
if ($parsedUrl['scheme'] === 'http' || $parsedUrl['scheme'] === 'https') {
|
||||
$host = $parsedUrl['host'];
|
||||
foreach ($this->siteFinder->getAllSites() as $site) {
|
||||
if ($site->getBase()->getHost() === $host) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the URL is relative to the current TYPO3 installation.
|
||||
*/
|
||||
protected function isRelativeUrl(RequestInterface $request, string $url): bool
|
||||
{
|
||||
$url = GeneralUtility::sanitizeLocalUrl($url, $request);
|
||||
if (!empty($url)) {
|
||||
$parsedUrl = @parse_url($url);
|
||||
if ($parsedUrl !== false && !isset($parsedUrl['scheme']) && !isset($parsedUrl['host'])) {
|
||||
// If the relative URL starts with a slash, we need to check if it's within the current site path
|
||||
return $parsedUrl['path'][0] !== '/' || str_starts_with($parsedUrl['path'], $request->getAttribute('normalizedParams')->getSitePath());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user