TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,51 @@
<?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\Core\PasswordPolicy\Event;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
/**
* Event is dispatched before the `ContextData` DTO is passed to the password policy validator.
*
* Note, that the `$userData` array will include user data available from the initiating class only.
* Event listeners should therefore always consider the initiating class name when accessing data
* from `getUserData()`.
*/
final readonly class EnrichPasswordValidationContextDataEvent
{
public function __construct(
private ContextData $contextData,
private array $userData,
private string $initiatingClass
) {}
public function getContextData(): ContextData
{
return $this->contextData;
}
public function getUserData(): array
{
return $this->userData;
}
public function getInitiatingClass(): string
{
return $this->initiatingClass;
}
}
@@ -0,0 +1,39 @@
<?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\Core\PasswordPolicy\Generator;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Exception\InvalidPasswordRulesException;
/**
* @internal only to be used within ext:core, not part of TYPO3 Core API.
*/
#[Autoconfigure(public: true)]
final readonly class PasswordGenerator implements PasswordGeneratorInterface
{
public function __construct(private Random $random) {}
/**
* @throws InvalidPasswordRulesException
*/
public function generate(array $options): string
{
return $this->random->generateRandomPassword($options);
}
}
@@ -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\Core\PasswordPolicy\Generator;
use TYPO3\CMS\Core\Exception\InvalidPasswordRulesException;
/**
* This is an interface that has to be used by all password generators.
*
* Each password generator needs to implement the generate method that returns the generated password.
* In case an invalid option/configuration is passed to the generator, an InvalidPasswordRulesException
* or LogicException needs to be thrown.
*/
interface PasswordGeneratorInterface
{
/**
* @throws InvalidPasswordRulesException
*/
public function generate(array $options): string;
}
+83
View File
@@ -0,0 +1,83 @@
<?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\Core\PasswordPolicy;
use TYPO3\CMS\Core\PasswordPolicy\Validator\AbstractPasswordValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Password policy class which holds information about configured password validators and password requirements
*
* @internal
*/
class PasswordPolicy
{
/**
* @var AbstractPasswordValidator[]
*/
protected array $validators = [];
/**
* @param array<class-string<AbstractPasswordValidator>, array<string, mixed>> $validators
*/
public function __construct(array $validators, protected PasswordPolicyAction $action)
{
foreach ($validators as $validatorClassName => $validatorSettings) {
// Exclude validator if current action is defined as excludeAction
if (in_array($action, $validatorSettings['excludeActions'] ?? [], true)) {
continue;
}
$this->validators[] = GeneralUtility::makeInstance(
$validatorClassName,
$validatorSettings['options'] ?? []
);
}
}
public function getAction(): PasswordPolicyAction
{
return $this->action;
}
public function hasValidators(): bool
{
return !empty($this->validators);
}
public function getValidators(): array
{
return $this->validators;
}
/**
* Returns an array with requirements (e.g. ["Password must at least contain one char"]) for all
* configured password validators. The structure of the array is as following:
*
* ['classId.validatorId' => 'Requirement text']
*/
public function getRequirements(): array
{
$requirements = [];
foreach ($this->validators as $validator) {
$requirements = array_merge($requirements, $validator->getRequirements());
}
return $requirements;
}
}
@@ -0,0 +1,29 @@
<?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\Core\PasswordPolicy;
/**
* This class contains actions which are used in password policy validators
*/
enum PasswordPolicyAction: string
{
case UPDATE_USER_PASSWORD = 'updateUserPassword';
case UPDATE_USER_PASSWORD_SWITCH_USER_MODE = 'updateUserPasswordSwitchUserMode';
case NEW_USER_PASSWORD = 'newUserPassword';
case UPDATE_INSTALL_TOOL_PASSWORD = 'updateInstallToolPassword';
}
@@ -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\Core\PasswordPolicy;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
/**
* Validates a password using validators configured in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'].
* The class must be instantiated with an action (see PasswordPolicyAction) and a password policy name.
*/
class PasswordPolicyValidator
{
protected ?PasswordPolicy $passwordPolicy = null;
protected array $validationErrors = [];
public function __construct(PasswordPolicyAction $action, string $passwordPolicy = 'default')
{
$passwordPolicies = $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] ?? [];
if (isset($passwordPolicies[$passwordPolicy])) {
$this->passwordPolicy = new PasswordPolicy(
$passwordPolicies[$passwordPolicy]['validators'] ?? [],
$action,
);
}
}
/**
* Returns, if the given password meets all requirements defined by configured password policy validators.
* If no password policy is set or the password policy has no validators, the given password is considered
* as valid.
*
* @param string $password The password to validate
* @param ContextData|null $contextData ContextData for usage in additional checks (e.g. password must not contain users firstname).
*/
public function isValidPassword(string $password, ?ContextData $contextData = null): bool
{
if (!$this->isEnabled()) {
return true;
}
$isValid = true;
foreach ($this->passwordPolicy->getValidators() as $validator) {
if (!$validator->validate($password, $contextData)) {
$this->validationErrors = array_merge($this->validationErrors, $validator->getErrorMessages());
$isValid = false;
}
}
return $isValid;
}
public function isEnabled(): bool
{
return $this->passwordPolicy !== null && $this->passwordPolicy->hasValidators();
}
public function hasRequirements(): bool
{
return !empty($this->getRequirements());
}
public function getRequirements(): array
{
return $this->passwordPolicy ? $this->passwordPolicy->getRequirements() : [];
}
public function getValidationErrors(): array
{
return $this->validationErrors;
}
}
@@ -0,0 +1,58 @@
<?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\Core\PasswordPolicy;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
use TYPO3\CMS\Core\Utility\GeneralUtility;
final readonly class PasswordService
{
/**
* @return array<string, string>
*/
public function getValidationErrorsForInstallToolUpdate(#[\SensitiveParameter] string $password): array
{
return $this->getValidationErrorsForPolicyAction(
$password,
'installTool',
PasswordPolicyAction::UPDATE_INSTALL_TOOL_PASSWORD,
);
}
/**
* @param string $password The password to validate
* @param string $passwordPolicyUsageContext Refers to a section in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'][$passwordPolicyUsage]['validators'][...]
* @param PasswordPolicyAction $passwordPolicyAction Policy action to perform (to indicate the action like "update Install Tool password")
* @param ContextData|null $contextData Optional context data (for example, previous/current password(s)) used within validators
*/
public function getValidationErrorsForPolicyAction(
#[\SensitiveParameter]
string $password,
string $passwordPolicyUsageContext,
PasswordPolicyAction $passwordPolicyAction,
?ContextData $contextData = null,
): array {
$passwordPolicyValidator = GeneralUtility::makeInstance(
PasswordPolicyValidator::class,
$passwordPolicyAction,
$passwordPolicyUsageContext,
);
$passwordPolicyValidator->isValidPassword($password, $contextData);
return $passwordPolicyValidator->getValidationErrors();
}
}
@@ -0,0 +1,121 @@
<?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\Core\PasswordPolicy\Validator;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Abstract password validator class, which all TYPO3 password validators must extend.
*/
abstract class AbstractPasswordValidator
{
private array $requirements = [];
private array $errorMessages = [];
public function __construct(protected array $options = [])
{
$this->initializeRequirements();
}
/**
* Function must be overwritten by extending classes in order to add requirements.
* Use `$this->addRequirement(string $identifier, string $message);` to add a requirement.
*/
public function initializeRequirements(): void {}
/**
* Validates the given password. Function must be overwritten by extending classes.
* If validation is considered as failed, use `addErrorMessage(string $identifier, string $errorMessage)`
* to add an error message and return `false`.
*
* @param string $password The password to validate
* @param ContextData|null $contextData ContextData for usage in additional checks (e.g. password must not contain users firstname).
*/
public function validate(string $password, ?ContextData $contextData = null): bool
{
return false;
}
/**
* Returns all requirements
*/
final public function getRequirements(): array
{
return array_map(htmlspecialchars(...), $this->requirements);
}
/**
* Adds a requirement with the given identifier and message.
*
* @param string $identifier Unique identifier for requirement
* @param string $message Message describing the requirement (e.g. "At least one digit")
*/
final protected function addRequirement(string $identifier, string $message): void
{
$classIdentifier = $this->getClassId();
$this->requirements[$classIdentifier . $identifier] = $message;
}
/**
* Returns all error messages
*/
final public function getErrorMessages(): array
{
return $this->errorMessages;
}
/**
* Adds an validation error message with the given identifier and message.
*
* @param string $identifier Unique identifier for error message
* @param string $errorMessage Message describing the error (e.g. "The password must at least contain one digit")
*/
final protected function addErrorMessage(string $identifier, string $errorMessage): void
{
$classIdentifier = $this->getClassId();
$this->errorMessages[$classIdentifier . $identifier] = $errorMessage;
}
private function getClassId(): string
{
$classParts = explode('\\', static::class);
return lcfirst(end($classParts)) . '.';
}
protected function getLanguageService(): LanguageService
{
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
if ($request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isFrontend()) {
$languageServiceFactory = GeneralUtility::makeInstance(LanguageServiceFactory::class);
return $languageServiceFactory->createFromSiteLanguage($request->getAttribute('language')
?? $request->getAttribute('site')->getDefaultLanguage());
}
if (($GLOBALS['LANG'] ?? null) instanceof LanguageService) {
return $GLOBALS['LANG'];
}
$languageServiceFactory = GeneralUtility::makeInstance(LanguageServiceFactory::class);
return $languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER'] ?? null);
}
}
@@ -0,0 +1,164 @@
<?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\Core\PasswordPolicy\Validator;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
/**
* Configurable TYPO3 core password validator which can validate, that a password has:
*
* - A minimum length
* - At least one upper case char
* - At least one lower case char
* - At least one digit
* - At least one special char
*
* @internal only to be used within ext:core, not part of TYPO3 Core API.
*/
class CorePasswordValidator extends AbstractPasswordValidator
{
public function validate(string $password, ?ContextData $contextData = null): bool
{
$isValid = true;
$lang = $this->getLanguageService();
if (strlen($password) < $this->getMinLength()) {
$this->addErrorMessage(
'minimumLength',
sprintf(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.minimumLength'),
$this->getMinLength()
)
);
$isValid = false;
}
if ($this->isCheckEnabled('upperCaseCharacterRequired')
&& !$this->evaluatePasswordRequirement($password, 'upperCaseCharacterRequired')
) {
$this->addErrorMessage(
'upperCaseCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.upperCaseCharacterRequired')
);
$isValid = false;
}
if ($this->isCheckEnabled('lowerCaseCharacterRequired')
&& !$this->evaluatePasswordRequirement($password, 'lowerCaseCharacterRequired')
) {
$this->addErrorMessage(
'lowerCaseCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.lowerCaseCharacterRequired')
);
$isValid = false;
}
if ($this->isCheckEnabled('digitCharacterRequired')
&& !$this->evaluatePasswordRequirement($password, 'digitCharacterRequired')
) {
$this->addErrorMessage(
'digitCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.digitCharacterRequired')
);
$isValid = false;
}
if ($this->isCheckEnabled('specialCharacterRequired')
&& !$this->evaluatePasswordRequirement($password, 'specialCharacterRequired')
) {
$this->addErrorMessage(
'specialCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.specialCharacterRequired')
);
$isValid = false;
}
return $isValid;
}
public function initializeRequirements(): void
{
$lang = $this->getLanguageService();
$this->addRequirement(
'minimumLength',
sprintf(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.minimumLength'),
$this->getMinLength()
),
);
if ($this->isCheckEnabled('upperCaseCharacterRequired')) {
$this->addRequirement(
'upperCaseCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.upperCaseCharacterRequired')
);
}
if ($this->isCheckEnabled('lowerCaseCharacterRequired')) {
$this->addRequirement(
'lowerCaseCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.lowerCaseCharacterRequired')
);
}
if ($this->isCheckEnabled('digitCharacterRequired')) {
$this->addRequirement(
'digitCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.digitCharacterRequired')
);
}
if ($this->isCheckEnabled('specialCharacterRequired')) {
$this->addRequirement(
'specialCharacterRequired',
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.specialCharacterRequired')
);
}
}
private function getMinLength(): int
{
return (int)($this->options['minimumLength'] ?? 8);
}
private function isCheckEnabled(string $checkIdentifier): bool
{
return $this->options[$checkIdentifier] ?? false;
}
/**
* Evaluates the password complexity for the given check
*/
private function evaluatePasswordRequirement(string $password, string $requirement): bool
{
$result = true;
$patterns = [
'upperCaseCharacterRequired' => '/[A-Z]/',
'lowerCaseCharacterRequired' => '/[a-z]/',
'digitCharacterRequired' => '/[0-9]/',
'specialCharacterRequired' => '/[^0-9a-z]/i',
];
if (isset($patterns[$requirement]) && !preg_match($patterns[$requirement], $password) > 0) {
$result = false;
}
return $result;
}
}
@@ -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\Core\PasswordPolicy\Validator\Dto;
/**
* Class with context data used in password validators. Uses internally an array with key/value pairs to store data.
* Extensions authors using this class should use `setData()` and `getData()` to write or read custom data used in
* custom password validators.
*
* @internal only to be used within ext:core, not part of TYPO3 Core API.
*/
class ContextData
{
protected array $data = [];
public function __construct(
string $loginMode = 'BE',
string $currentPasswordHash = '',
string $newUsername = '',
string $newUserFirstName = '',
string $newUserLastName = '',
string $newUserFullName = '',
) {
$this->data['loginMode'] = $loginMode;
$this->data['currentPasswordHash'] = $currentPasswordHash;
$this->data['newUsername'] = $newUsername;
$this->data['newUserFirstName'] = $newUserFirstName;
$this->data['newUserLastName'] = $newUserLastName;
$this->data['newUserFullName'] = $newUserFullName;
}
public function getLoginMode(): string
{
return $this->getData('loginMode');
}
public function getCurrentPasswordHash(): string
{
return $this->getData('currentPasswordHash');
}
public function getNewUsername(): string
{
return $this->getData('newUsername');
}
public function getNewUserFirstName(): string
{
return $this->getData('newUserFirstName');
}
public function getNewUserLastName(): string
{
return $this->getData('newUserLastName');
}
public function getNewUserFullName(): string
{
return $this->getData('newUserFullName');
}
public function getData(string $key): string
{
return $this->data[$key] ?? '';
}
public function setData(string $key, string $value): void
{
$this->data[$key] = $value;
}
}
@@ -0,0 +1,81 @@
<?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\Core\PasswordPolicy\Validator;
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\PasswordPolicy\Validator\Dto\ContextData;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This validator checks, if the given password matches the current user password
*
* @internal only to be used within ext:core, not part of TYPO3 Core API.
*/
class NotCurrentPasswordValidator extends AbstractPasswordValidator
{
public function validate(string $password, ?ContextData $contextData = null): bool
{
if (!$contextData) {
throw new \RuntimeException('ContextData must be supplied to validator.', 1662808782);
}
if (in_array($contextData->getLoginMode(), ['FE', 'BE'], true)) {
$isValid = !$this->isCurrentPassword($password, $contextData);
} else {
throw new \RuntimeException('Unsupported loginMode provided. Ensure, that loginMode is either "FE" or "BE".', 1649846004);
}
return $isValid;
}
public function initializeRequirements(): void
{
$this->addRequirement(
'notCurrentPassword',
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:requirement.notCurrentPassword')
);
}
/**
* Returns if the hash of the given password equals the hash of the current password
*/
protected function isCurrentPassword(string $password, ContextData $contextData): bool
{
$result = false;
$saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
try {
$hashInstance = $saltFactory->get($contextData->getCurrentPasswordHash(), $contextData->getLoginMode());
$result = $hashInstance->checkPassword(
$password,
$contextData->getCurrentPasswordHash()
);
} catch (InvalidPasswordHashException $e) {
// Since the password will be updated, we silently ignore, if current password hash can not be checked
}
if ($result) {
$this->addErrorMessage(
'notCurrentPassword',
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:error.notCurrentPassword')
);
}
return $result;
}
}