TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
/*
* 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\Extbase\Validation;
/**
* This object holds a validation error.
*/
class Error extends \TYPO3\CMS\Extbase\Error\Error
{
/**
* @var string
*/
protected $message = 'Unknown validation error';
/**
* @var int
*/
protected $code = 1201447005;
}
+25
View File
@@ -0,0 +1,25 @@
<?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\Extbase\Validation;
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
/**
* A generic validation exception
*/
class Exception extends ExtbaseException {}
@@ -0,0 +1,25 @@
<?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\Extbase\Validation\Exception;
use TYPO3\CMS\Extbase\Validation\Exception;
/**
* An "Invalid TypeHint" Exception
*/
class InvalidTypeHintException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Validation\Exception;
use TYPO3\CMS\Extbase\Validation\Exception;
/**
* A "InvalidValidationConfiguration" Exception
*/
class InvalidValidationConfigurationException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Validation\Exception;
use TYPO3\CMS\Extbase\Validation\Exception;
/**
* A "InvalidValidationOptions" Exception
*/
class InvalidValidationOptionsException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Validation\Exception;
use TYPO3\CMS\Extbase\Validation\Exception;
/**
* A "NoSuchValidator" Exception
*/
class NoSuchValidatorException extends Exception {}
@@ -0,0 +1,145 @@
<?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\Extbase\Validation\Validator;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
/**
* An abstract composite validator consisting of other validators
*/
abstract class AbstractCompositeValidator implements ObjectValidatorInterface, \Countable
{
/**
* This contains the supported options, their default values and descriptions.
*
* @var array
*/
protected $supportedOptions = [];
protected array $options = [];
/**
* @var \SplObjectStorage<ValidatorInterface>
*/
protected \SplObjectStorage $validators;
protected ?ServerRequestInterface $request = null;
protected \SplObjectStorage $validatedInstancesContainer;
public function setOptions(array $options): void
{
$this->initializeDefaultOptions($options);
}
/**
* Adds a new validator to the composition.
*/
public function addValidator(ValidatorInterface $validator): void
{
$this->validators->offsetSet($validator);
}
public function getRequest(): ?ServerRequestInterface
{
return $this->request;
}
public function setRequest(?ServerRequestInterface $request): void
{
$this->request = $request;
}
/**
* Removes the specified validator.
*
* @throws NoSuchValidatorException
*/
public function removeValidator(ValidatorInterface $validator): void
{
if (!$this->validators->offsetExists($validator)) {
throw new NoSuchValidatorException('Cannot remove validator because its not in the conjunction.', 1207020177);
}
$this->validators->offsetUnset($validator);
}
/**
* Returns the number of validators contained in this composition.
*/
public function count(): int
{
return count($this->validators);
}
/**
* Returns the child validators of this Composite Validator
*
* @return \SplObjectStorage<ValidatorInterface>
*/
public function getValidators(): \SplObjectStorage
{
return $this->validators;
}
/**
* Returns the options for this validator
*/
public function getOptions(): array
{
return $this->options;
}
/**
* Allows to set a container to keep track of validated instances.
*/
public function setValidatedInstancesContainer(\SplObjectStorage $validatedInstancesContainer): void
{
$this->validatedInstancesContainer = $validatedInstancesContainer;
}
/**
* Initialize default options.
* @throws InvalidValidationOptionsException
*/
protected function initializeDefaultOptions(array $options): void
{
// check for options given but not supported
if (($unsupportedOptions = array_diff_key($options, $this->supportedOptions)) !== []) {
throw new InvalidValidationOptionsException('Unsupported validation option(s) found: ' . implode(', ', array_keys($unsupportedOptions)), 1339079804);
}
// check for required options being set
array_walk(
$this->supportedOptions,
static function (array $supportedOptionData, string $supportedOptionName, array $options): void {
if (isset($supportedOptionData[3]) && !array_key_exists($supportedOptionName, $options)) {
throw new InvalidValidationOptionsException('Required validation option not set: ' . $supportedOptionName, 1339163922);
}
},
$options
);
// merge with default values
$this->options = array_merge(
array_map(
static fn(array $value): mixed => $value[0],
$this->supportedOptions
),
$options
);
$this->validators = new \SplObjectStorage();
}
}
@@ -0,0 +1,178 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
/**
* A generic object validator which allows for specifying property validators.
*/
abstract class AbstractGenericObjectValidator extends AbstractValidator implements ObjectValidatorInterface
{
/**
* @var array<string, \SplObjectStorage<ValidatorInterface>>
*/
protected array $propertyValidators = [];
/**
* @var \SplObjectStorage
*/
protected $validatedInstancesContainer;
/**
* Checks if the given value is valid according to the validator, and returns
* the Error Messages object which occurred.
*
* @param mixed $value The value that should be validated
*/
public function validate(mixed $value): Result
{
if (is_object($value) && $this->isValidatedAlready($value)) {
return $this->result;
}
$this->result = new Result();
if ($this->acceptsEmptyValues === false || $this->isEmpty($value) === false) {
if (!is_object($value)) {
$this->addError('Object expected, %1$s given.', 1241099149, [gettype($value)]);
} elseif ($this->isValidatedAlready($value) === false) {
$this->markInstanceAsValidated($value);
$this->isValid($value);
}
}
return $this->result;
}
/**
* Load the property value to be used for validation.
* In case the object is a doctrine proxy, we need to load the real instance first.
*/
protected function getPropertyValue(object $object, string $propertyName): mixed
{
if (ObjectAccess::isPropertyGettable($object, $propertyName)) {
return ObjectAccess::getProperty($object, $propertyName);
}
throw new \RuntimeException(
sprintf(
'Could not get value of property "%s::%s", make sure the property is either public or has a getter get%3$s(), a hasser has%3$s() or an isser is%3$s().',
get_class($object),
$propertyName,
ucfirst($propertyName)
),
1546632293
);
}
/**
* Checks if the specified property of the given object is valid, and adds
* found errors to the $messages object.
*
* @param \Traversable<ValidatorInterface> $validators
*/
protected function checkProperty(mixed $value, \Traversable $validators, string $propertyName): void
{
/** @var Result|null $result */
$result = null;
foreach ($validators as $validator) {
if ($validator instanceof ObjectValidatorInterface) {
$validator->setValidatedInstancesContainer($this->validatedInstancesContainer);
}
$currentResult = $validator->validate($value);
if ($currentResult->hasMessages()) {
if ($result == null) {
$result = $currentResult;
} else {
$result->merge($currentResult);
}
}
}
if ($result != null) {
$this->result->forProperty($propertyName)->merge($result);
}
}
/**
* Checks if the given value is valid according to the property validators.
*/
protected function isValid(mixed $object): void
{
foreach ($this->propertyValidators as $propertyName => $validators) {
$propertyValue = $this->getPropertyValue($object, $propertyName);
$this->checkProperty($propertyValue, $validators, $propertyName);
}
}
/**
* Checks the given object can be validated by the validator implementation
*/
public function canValidate(mixed $object): bool
{
return is_object($object);
}
/**
* Adds the given validator for validation of the specified property.
*/
public function addPropertyValidator(string $propertyName, ValidatorInterface $validator): void
{
if (!isset($this->propertyValidators[$propertyName])) {
$this->propertyValidators[$propertyName] = new \SplObjectStorage();
}
$this->propertyValidators[$propertyName]->offsetSet($validator);
}
protected function isValidatedAlready(object $object): bool
{
if ($this->validatedInstancesContainer === null) {
$this->validatedInstancesContainer = new \SplObjectStorage();
}
if ($this->validatedInstancesContainer->offsetExists($object)) {
return true;
}
return false;
}
protected function markInstanceAsValidated(object $object): void
{
$this->validatedInstancesContainer->offsetSet($object);
}
/**
* Returns all property validators - or only validators of the specified property
*
* @return ($propertyName is null ? array<string, \SplObjectStorage<ValidatorInterface>> : \SplObjectStorage<ValidatorInterface>)
*/
public function getPropertyValidators(?string $propertyName = null): array|\SplObjectStorage
{
if ($propertyName !== null) {
return $this->propertyValidators[$propertyName] ?? [];
}
return $this->propertyValidators;
}
/**
* Allows to set a container to keep track of validated instances.
*/
public function setValidatedInstancesContainer(\SplObjectStorage $validatedInstancesContainer): void
{
$this->validatedInstancesContainer = $validatedInstancesContainer;
}
}
@@ -0,0 +1,231 @@
<?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\Extbase\Validation\Validator;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Type\File\FileInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Extbase\Validation\Error;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Abstract validator. Mother af most validators.
*/
abstract class AbstractValidator implements ValidatorInterface
{
/**
* Specifies whether this validator accepts empty values.
*
* If this is TRUE, the validators isValid() method is not called in case of an empty value
* Note: A value is considered empty if it is NULL or an empty string!
* By default, all validators except for NotEmpty and the Composite Validators accept empty values.
*
* @var bool
*/
protected $acceptsEmptyValues = true;
/**
* Contains an array of property names used for translation handling of error messages.
*/
protected array $translationOptions = ['message'];
/**
* This contains the supported options, their default values, types and descriptions.
*
* @var array
*/
protected $supportedOptions = [];
protected array $options = [];
protected Result $result;
protected ?ServerRequestInterface $request = null;
public function setOptions(array $options): void
{
$this->initializeDefaultOptions($options);
$this->initializeTranslationOptions($options);
}
public function setRequest(?ServerRequestInterface $request): void
{
$this->request = $request;
}
/**
* Checks if the given value is valid according to the validator, and returns
* the error messages object which occurred.
*
* @param mixed $value The value that should be validated
*/
public function validate(mixed $value): Result
{
$this->result = new Result();
if ($this->acceptsEmptyValues === false || $this->isEmpty($value) === false) {
$this->isValid($value);
}
return $this->result;
}
/**
* Check if $value is valid. If it is not valid, needs to add an error to result.
*/
abstract protected function isValid(mixed $value): void;
/**
* Creates a new validation error object and adds it to $this->result
*
* @param string $message The error message
* @param int $code The error code (a unix timestamp)
* @param array $arguments Arguments to be replaced in message
* @param string $title title of the error
*/
protected function addError(string $message, int $code, array $arguments = [], string $title = ''): void
{
$this->result->addError(new Error($message, $code, $arguments, $title));
}
/**
* Creates a new validation error object for a property and adds it to the proper sub result of $this->result
*
* @param string|array $propertyPath The property path (string or array)
* @param string $message The error message
* @param int $code The error code (a unix timestamp)
* @param array $arguments Arguments to be replaced in message
* @param string $title Title of the error
*/
protected function addErrorForProperty(string|array $propertyPath, string $message, int $code, array $arguments = [], string $title = ''): void
{
$propertyPath = is_array($propertyPath) ? implode('.', $propertyPath) : $propertyPath;
$error = new Error($message, $code, $arguments, $title);
$this->result->forProperty($propertyPath)->addError($error);
}
/**
* Returns the options of this validator
*/
public function getOptions(): array
{
return $this->options;
}
public function getRequest(): ?ServerRequestInterface
{
return $this->request;
}
/**
* TRUE if the given $value is NULL or an empty string ('')
*/
final protected function isEmpty(mixed $value): bool
{
return $value === null || $value === '';
}
/**
* Translates an error message using LocalizationUtility::translate() method. If the translate key does not
* start with 'LLL:' and if no extension name is provided, the original translate key is returned.
*/
protected function translateErrorMessage(
string $translateKey,
string $extensionName = '',
array $arguments = []
): string {
if ($extensionName === '' && !str_starts_with($translateKey, 'LLL:')) {
return $translateKey;
}
return LocalizationUtility::translate(
$translateKey,
$extensionName,
$arguments
) ?? '';
}
/**
* Initialize default options.
* @throws InvalidValidationOptionsException
*/
protected function initializeDefaultOptions(array $options): void
{
// check for options given but not supported
if (($unsupportedOptions = array_diff_key($options, $this->supportedOptions)) !== []) {
throw new InvalidValidationOptionsException('Unsupported validation option(s) found: ' . implode(', ', array_keys($unsupportedOptions)), 1379981890);
}
// check for required options being set
array_walk(
$this->supportedOptions,
static function (array $supportedOptionData, string $supportedOptionName, array $options): void {
if (isset($supportedOptionData[3]) && $supportedOptionData[3] === true && !array_key_exists($supportedOptionName, $options)) {
throw new InvalidValidationOptionsException('Required validation option not set: ' . $supportedOptionName, 1379981891);
}
},
$options
);
// merge with default values
$this->options = array_merge(
array_map(
static fn(array $value): mixed => $value[0],
$this->supportedOptions
),
$options
);
}
/**
* Ensures that the provided value is either an instance of UploadedFile or an ObjectStorage containing only
* UploadedFile instances.
*/
protected function ensureFileUploadTypes(mixed $value): void
{
if ($value instanceof UploadedFile) {
return;
}
if ($value instanceof ObjectStorage) {
foreach ($value as $uploadedFile) {
if (!$uploadedFile instanceof UploadedFile) {
throw new \InvalidArgumentException('Value to validate must be an ObjectStorage of TYPO3\\CMS\\Core\\Http\\UploadedFile', 1722763902);
}
}
return;
}
throw new \InvalidArgumentException('Value to validate must be a TYPO3\\CMS\\Core\\Http\\UploadedFile', 1712057926);
}
protected function getFileInfo(string $filePath): FileInfo
{
return GeneralUtility::makeInstance(FileInfo::class, $filePath);
}
/**
* Initializes all registered translation options with custom translation options from the given options array
*/
protected function initializeTranslationOptions(array $options): void
{
foreach ($this->translationOptions as $translationOption) {
if (property_exists($this, $translationOption)) {
$this->$translationOption = $options[$translationOption] ?? $this->$translationOption;
}
}
}
}
@@ -0,0 +1,40 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for alphanumeric strings
*/
final class AlphanumericValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.alphanumeric.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* The given $value is valid if it is an alphanumeric string, which is defined as [\pL\d]*.
*/
public function isValid(mixed $value): void
{
if (!is_string($value) || preg_match('/^[\pL\d]*$/u', $value) !== 1) {
$this->addError($this->translateErrorMessage($this->message), 1221551320);
}
}
}
@@ -0,0 +1,84 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for boolean values
*/
final class BooleanValidator extends AbstractValidator
{
protected string $notTrueMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.boolean.nottrue';
protected string $notFalseMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.boolean.notfalse';
protected array $translationOptions = ['notTrueMessage', 'notFalseMessage'];
/**
* @var array
*/
protected $supportedOptions = [
// The default is set to NULL here, because we need to be backward compatible here, because this
// BooleanValidator is called automatically on boolean action arguments. If we would set it to TRUE,
// every FALSE value for an action argument would break.
// @todo with next patches: deprecate this BooleanValidator and introduce a BooleanValueValidator, like
// in Flow, which won't be called on boolean action arguments.
'is' => [null, 'Boolean value', 'boolean|string|integer'],
'notTrueMessage' => [null, 'Translation key or message for not true value', 'string'],
'notFalseMessage' => [null, 'Translation key or message for not false value', 'string'],
];
/**
* Check if $value matches the expectation given to the validator.
* If it does not match, the function adds an error to the result.
*
* Also testing for '1' (true), '0' and '' (false) because casting varies between
* tests and actual usage. This makes the validator loose but still keeping functionality.
*/
public function isValid(mixed $value): void
{
// see comment above, check if expectation is NULL, then nothing to do!
if ($this->options['is'] === null) {
return;
}
switch (strtolower((string)$this->options['is'])) {
case 'true':
case '1':
$expectation = true;
break;
case 'false':
case '':
case '0':
$expectation = false;
break;
default:
$this->addError('The given expectation is not valid.', 1361959227);
return;
}
if ($value !== $expectation) {
if (!is_bool($value)) {
$this->addError($this->translateErrorMessage($this->notTrueMessage), 1361959230);
} else {
if ($expectation) {
$this->addError($this->translateErrorMessage($this->notTrueMessage), 1361959228);
} else {
$this->addError($this->translateErrorMessage($this->notFalseMessage), 1361959229);
}
}
}
}
}
@@ -0,0 +1,91 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
/**
* A generic collection validator.
*/
final class CollectionValidator extends AbstractGenericObjectValidator
{
/**
* @var array
*/
protected $supportedOptions = [
'elementValidator' => [null, 'The validator type to use for the collection elements', 'string'],
'elementType' => [null, 'The type of the elements in the collection', 'string'],
];
public function __construct(private readonly ValidatorResolver $validatorResolver) {}
/**
* Checks if the given value is valid according to the validator, and returns
* the Error Messages object which occurred.
*/
public function validate(mixed $value): Result
{
$this->result = new Result();
if ($this->acceptsEmptyValues === false || $this->isEmpty($value) === false) {
if ((is_object($value) && !TypeHandlingUtility::isCollectionType(get_class($value))) && !is_array($value)) {
$this->addError('The given subject was not a collection.', 1317204797);
return $this->result;
}
if ($value instanceof LazyObjectStorage && !$value->isInitialized()) {
return $this->result;
}
if (is_object($value)) {
if ($this->isValidatedAlready($value)) {
return $this->result;
}
$this->markInstanceAsValidated($value);
}
$this->isValid($value);
}
return $this->result;
}
/**
* Checks for a collection and if needed validates the items in the collection.
* This is done with the specified element validator or a validator based on
* the given element type.
*
* Either elementValidator or elementType must be given, otherwise validation
* will be skipped.
*/
protected function isValid(mixed $value): void
{
foreach ($value as $index => $collectionElement) {
if (isset($this->options['elementValidator'])) {
$collectionElementValidator = $this->validatorResolver->createValidator($this->options['elementValidator']);
} elseif (isset($this->options['elementType'])) {
$collectionElementValidator = $this->validatorResolver->getBaseValidatorConjunction($this->options['elementType']);
} else {
return;
}
if ($collectionElementValidator instanceof ObjectValidatorInterface) {
$collectionElementValidator->setValidatedInstancesContainer($this->validatedInstancesContainer);
}
$this->result->forProperty((string)$index)->merge($collectionElementValidator->validate($collectionElement));
}
}
}
@@ -0,0 +1,48 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Error\Result;
/**
* Validator to chain many validators in a conjunction (logical and).
*/
final class ConjunctionValidator extends AbstractCompositeValidator
{
public function __construct()
{
$this->validators = new \SplObjectStorage();
$this->validatedInstancesContainer = new \SplObjectStorage();
}
/**
* Checks if the given value is valid according to the validators of the conjunction.
* Every validator has to be valid, to make the whole conjunction valid.
*
* @param mixed $value The value that should be validated
*/
public function validate(mixed $value): Result
{
$result = new Result();
foreach ($this->getValidators() as $validator) {
$result->merge($validator->validate($value));
}
return $result;
}
}
@@ -0,0 +1,123 @@
<?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\Extbase\Validation\Validator;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validation;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Validator that decorates a Symfony Constraint.
*/
#[Exclude]
final readonly class ConstraintDecoratingValidator implements ValidatorInterface
{
public function __construct(
private Constraint $constraint,
) {}
public function validate(mixed $value): Result
{
$validator = Validation::createValidatorBuilder()->disableTranslation()->getValidator();
$constraintViolationList = $validator->validate($value, $this->constraint);
$result = new Result();
foreach ($constraintViolationList as $constraintViolation) {
$arguments = array_values($constraintViolation->getParameters());
$code = $this->convertConstraintViolationCode($constraintViolation->getCode());
$messageTemplate = $this->convertConstraintViolationMessageTemplate($constraintViolation);
$result->addError(
new Error(
$this->translateErrorMessage($messageTemplate, $arguments),
$code,
$arguments,
),
);
}
return $result;
}
/**
* Convert UUID-based violation code to an integer.
*/
private function convertConstraintViolationCode(?string $code): int
{
if ($code !== null && $code !== '') {
$hash = hash('sha256', $code);
$code = hexdec(substr($hash, 0, 8));
}
return (int)$code;
}
/**
* Convert named placeholders like {{ value }} to sprintf compatible placeholders like %1$s.
*
* Before: 'The value {{ value }} must follow the {{ format }} format.'
* After: 'The value %1$s must follow the %2$s format.'
*/
private function convertConstraintViolationMessageTemplate(ConstraintViolationInterface $constraintViolation): string
{
$placeholderMap = [];
foreach (array_keys($constraintViolation->getParameters()) as $index => $placeholder) {
$placeholderMap[$placeholder] = '%' . ($index + 1) . '$s';
}
return strtr($constraintViolation->getMessageTemplate(), $placeholderMap);
}
/**
* @param list<scalar|\Stringable> $arguments
*/
private function translateErrorMessage(string $translateKey, array $arguments = []): string
{
if (!str_starts_with($translateKey, 'LLL:')) {
return $translateKey;
}
return LocalizationUtility::translate($translateKey, null, $arguments) ?? '';
}
public function setOptions(array $options): void
{
// Intentionally left blank.
}
public function getOptions(): array
{
return [];
}
public function setRequest(?ServerRequestInterface $request): void
{
// Intentionally left blank.
}
public function getRequest(): ?ServerRequestInterface
{
return null;
}
}
@@ -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\Extbase\Validation\Validator;
/**
* Validator for DateTime/DateTimeImmutable objects.
*/
final class DateTimeValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.datetime.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid DateTime object. If this is not
* the case, the function adds an error.
*/
public function isValid(mixed $value): void
{
$this->result->clear();
if ($value instanceof \DateTimeInterface) {
return;
}
$this->addError(
$this->translateErrorMessage(
$this->message,
'',
[
gettype($value),
]
),
1238087674,
[gettype($value)]
);
}
}
@@ -0,0 +1,70 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Error\Result;
/**
* Validator to chain many validators in a disjunction (logical or).
*/
final class DisjunctionValidator extends AbstractCompositeValidator
{
public function __construct()
{
$this->validators = new \SplObjectStorage();
$this->validatedInstancesContainer = new \SplObjectStorage();
}
/**
* Checks if the given value is valid according to the validators of the
* disjunction.
*
* So only one validator has to be valid, to make the whole disjunction valid.
* Errors are only returned if all validators failed.
*
* @param mixed $value The value that should be validated
*/
public function validate(mixed $value): Result
{
$validators = $this->getValidators();
if ($validators->count() > 0) {
$result = null;
foreach ($validators as $validator) {
$validatorResult = $validator->validate($value);
if ($validatorResult->hasErrors()) {
if ($result === null) {
$result = $validatorResult;
} else {
$result->merge($validatorResult);
}
} else {
if ($result === null) {
$result = $validatorResult;
} else {
$result->clear();
}
break;
}
}
} else {
$result = new Result();
}
return $result;
}
}
@@ -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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Validator for email addresses
*/
final class EmailAddressValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.emailaddress.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid email address.
*/
public function isValid(mixed $value): void
{
if (!is_string($value) || !$this->validEmail($value)) {
$this->addError($this->translateErrorMessage($this->message), 1221559976);
}
}
/**
* Checking syntax of input email address
*
* @param string $emailAddress Input string to evaluate
* @return bool Returns TRUE if the $email address (input string) is valid
*/
private function validEmail(string $emailAddress): bool
{
return GeneralUtility::validEmail($emailAddress);
}
}
@@ -0,0 +1,96 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Resource\MimeTypeCompatibilityTypeGuesser;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* Validator comparing file extension to the expected mime-type of a given UploadedFile
* or ObjectStorage of UploadedFile objects.
*/
final class FileExtensionMimeTypeConsistencyValidator extends AbstractValidator
{
protected string $notAllowedMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.fileextensionmimetypeconsistency.notallowed';
protected array $translationOptions = ['inconsistentMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'notAllowedMessage' => [null, 'Translation key or message for inconsistent mime-type for file extension', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
$fileInfo = $this->getFileInfo($uploadedFile->getTemporaryFileName());
$mimeType = $fileInfo->getMimeType();
// The file extension of the uploaded file must match the mime-type for this file.
// Example: myfile.txt is actually a PDF file (defined by mime-type), but .txt is not associated
// for application/pdf, so this is not valid.
$fileExtension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
$assumedMimesTypeOfFileExtension = (new MimeTypeDetector())->getMimeTypesForFileExtension($fileExtension);
// pass, in case no assumed mime-type was found (e.g., for individual file extension)
if ($mimeType === '' || $assumedMimesTypeOfFileExtension === []) {
return;
}
// Example in case of "exe", which has over 9000 possible MIME types:
// mime-db/db.json is only aware of "application/octet-stream", "application/x-msdos-program", "application/x-msdownload"
// However, PHP detects this as "application/vnd.microsoft.portable-executable" (PHP 8.4+) or "application/x-dosexec" (PHP < 8.4)
// DefaultConfiguration $GLOBALS['TYPO3_CONF_VARS']['SYS']['FileInfo']['mimeTypeCompatibility'] registers this (and some more),
// so we also need these fallbacks to be evaluated.
$mimeTypeCompatibility = (new MimeTypeCompatibilityTypeGuesser())->getMimeTypeCompatibilityList();
$additionalMappedMimeType = $mimeTypeCompatibility[$mimeType][$fileExtension] ?? null;
if (!in_array($mimeType, $assumedMimesTypeOfFileExtension, true)
&& !in_array($additionalMappedMimeType, $assumedMimesTypeOfFileExtension, true)
) {
$message = $this->translateErrorMessage(
$this->notAllowedMessage,
'',
[$mimeType, $fileExtension]
);
$code = 1754045716;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
}
@@ -0,0 +1,108 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator file extensions of a given UploadedFile or ObjectStorage of UploadedFile objects.
*/
final class FileExtensionValidator extends AbstractValidator
{
protected string $notAllowedMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.fileextension.notallowed';
protected array $translationOptions = ['notAllowedMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'allowedFileExtensions' => [null, 'Allowed file extensions', 'array'],
'useStorageDefaults' => [null, 'Whether to use the default allowed file extension of the storage', 'bool'],
'notAllowedMessage' => [null, 'Translation key or message for disallowed file extension', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
$this->validateOptions();
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
$allowedFileExtensions = [];
if (!empty($this->options['useStorageDefaults'])) {
$allowedFileExtensions = GeneralUtility::trimExplode(
',',
($GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] ?? '') . ','
. ($GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] ?? '') . ','
. ($GLOBALS['TYPO3_CONF_VARS']['SYS']['miscfile_ext'] ?? ''),
true
);
}
if (is_array($this->options['allowedFileExtensions'] ?? null)) {
$allowedFileExtensions = array_merge($allowedFileExtensions, $this->options['allowedFileExtensions']);
}
$allowedFileExtensions = array_map(mb_strtolower(...), $allowedFileExtensions);
$fileExtension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
if (!in_array($fileExtension, $allowedFileExtensions, true)) {
$message = $this->translateErrorMessage(
$this->notAllowedMessage,
'',
[$fileExtension]
);
$code = 1754043401;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
/**
* Checks if this validator is correctly configured
*/
private function validateOptions(): void
{
$hasAllowedFileExtensions = is_array($this->options['allowedFileExtensions'] ?? null)
&& $this->options['allowedFileExtensions'] !== [];
$shallUseStorageDefaults = !empty($this->options['useStorageDefaults']);
if (!$hasAllowedFileExtensions && !$shallUseStorageDefaults) {
throw new InvalidValidationOptionsException(
'Either the option "allowedFileExtensions" must be an array with at least one item, '
. 'or the option "useStorageDefaults" must be enabled.',
1754043328
);
}
}
}
@@ -0,0 +1,64 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Resource\Security\FileNameValidator as CoreFileNameValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
/**
* Validates, that the given UploadedFile or ObjectStorage of UploadedFile objects does not contain a php
* executable file by checking the given file extension.
*/
final class FileNameValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.denyphpupload.notallowed';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
if (!GeneralUtility::makeInstance(CoreFileNameValidator::class)->isValid($uploadedFile->getClientFilename())) {
$message = $this->translateErrorMessage($this->message);
$code = 1711367029;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
}
@@ -0,0 +1,114 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for file size of a given UploadedFile or ObjectStorage of UploadedFile objects.
*/
final class FileSizeValidator extends AbstractValidator
{
protected string $lessMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.filesize.less';
protected string $exceedMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.filesize.exceed';
protected array $translationOptions = ['lessMessage', 'exceedMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'minimum' => ['0B', 'The minimum file size to accept', 'string'],
'maximum' => [PHP_INT_MAX . 'B', 'The maximum file size to accept', 'string'],
'lessMessage' => [null, 'Translation key or message for value less than minimum', 'string'],
'exceedMessage' => [null, 'Translation key or message for value exceeds maximum', 'string'],
'byteSizeUnits' => [' Bytes| Kilobyte| Megabyte| Gigabyte', 'Byte size units string for "formatSize" function', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
$this->validateOptions();
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
$fileSize = $this->getFileInfo($uploadedFile->getTemporaryFileName())->getSize();
$minFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['minimum']);
if ($this->options['maximum'] !== PHP_INT_MAX . 'B') {
$maxFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['maximum']);
} else {
$maxFileSize = PHP_INT_MAX;
}
$labels = htmlspecialchars($this->options['byteSizeUnits']);
if ($fileSize < $minFileSize) {
$message = $this->translateErrorMessage(
$this->lessMessage,
'',
[GeneralUtility::formatSize($minFileSize, $labels)]
);
$code = 1708595754;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if ($fileSize > $maxFileSize) {
$message = $this->translateErrorMessage(
$this->exceedMessage,
'',
[GeneralUtility::formatSize($maxFileSize, $labels)]
);
$code = 1708595755;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
/**
* Checks if this validator is correctly configured
*/
private function validateOptions(): void
{
if (!preg_match('/^(\d*\.?\d+)(B|K|M|G)$/i', $this->options['minimum'])) {
throw new InvalidValidationOptionsException('The option "minimum" has an invalid format. Valid formats are something like this: "10B|K|M|G".', 1708595605);
}
if (!preg_match('/^(\d*\.?\d+)(B|K|M|G)$/i', $this->options['maximum'])) {
throw new InvalidValidationOptionsException('The option "maximum" has an invalid format. Valid formats are something like this: "10B|K|M|G".', 1708595606);
}
}
}
@@ -0,0 +1,46 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for floats.
*/
final class FloatValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.float.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* The given value is valid if it is of type float or a string matching the regular expression [0-9.e+-]
*
* @param mixed $value The value that should be validated
*/
public function isValid(mixed $value): void
{
if (is_float($value)) {
return;
}
if (!is_string($value) || !str_contains($value, '.') || preg_match('/^[0-9.e+-]+$/', $value) !== 1) {
$this->addError($this->translateErrorMessage($this->message), 1221560288);
}
}
}
@@ -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\Extbase\Validation\Validator;
/**
* A generic object validator which allows for specifying property validators.
* This is used as default model validator.
*/
final class GenericObjectValidator extends AbstractGenericObjectValidator {}
@@ -0,0 +1,191 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Type\File\ImageInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator to validate image dimensions of a given UploadedFile or ObjectStorage of UploadedFile objects.
*/
final class ImageDimensionsValidator extends AbstractValidator
{
protected string $widthMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.width.notvalid';
protected string $heightMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.height.notvalid';
protected string $minWidthMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.minwidth.notvalid';
protected string $minHeightMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.minheight.notvalid';
protected string $maxWidthMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.maxwidth.notvalid';
protected string $maxHeightMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.imagedimensions.maxheight.notvalid';
protected array $translationOptions = [
'widthMessage',
'heightMessage',
'minWidthMessage',
'minHeightMessage',
'maxWidthMessage',
'maxHeightMessage',
];
/**
* @var array
*/
protected $supportedOptions = [
'width' => [null, 'The exact width of the image', 'int'],
'height' => [null, 'The exact height of the image', 'int'],
'minWidth' => [0, 'The minimum width of the image', 'int'],
'maxWidth' => [PHP_INT_MAX, 'The maximum width of the image', 'int'],
'minHeight' => [0, 'The minimum height of the image', 'int'],
'maxHeight' => [PHP_INT_MAX, 'The maximum heigt of the image', 'int'],
'heightMessage' => [null, 'Translation key or message for invalid height', 'string'],
'widthMessage' => [null, 'Translation key or message for invalid width', 'string'],
'minWidthMessage' => [null, 'Translation key or message for invalid minimum width', 'string'],
'maxWidthMessage' => [null, 'Translation key or message for invalid maximum width', 'string'],
'minHeightMessage' => [null, 'Translation key or message for invalid minimum height', 'string'],
'maxHeightMessage' => [null, 'Translation key or message for invalid maximum height', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
$this->validateOptions();
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
$imageInfo = $this->getImageInfo($uploadedFile->getTemporaryFileName());
if ($imageInfo->getWidth() === 0 || $imageInfo->getHeight() === 0) {
// Silently ignore files, where the width or height could not be determined. Most likely no image file.
return;
}
if (isset($this->options['width']) && (int)$this->options['width'] !== $imageInfo->getWidth()) {
$message = $this->translateErrorMessage(
$this->widthMessage,
'',
[$this->options['width']]
);
$code = 1715964040;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if (isset($this->options['height']) && (int)$this->options['height'] !== $imageInfo->getHeight()) {
$message = $this->translateErrorMessage(
$this->heightMessage,
'',
[$this->options['height']]
);
$code = 1715964041;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if ((int)$this->options['minWidth'] > $imageInfo->getWidth()) {
$message = $this->translateErrorMessage(
$this->minWidthMessage,
'',
[$this->options['minWidth']]
);
$code = 1715964042;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if ((int)$this->options['minHeight'] > $imageInfo->getHeight()) {
$message = $this->translateErrorMessage(
$this->minHeightMessage,
'',
[$this->options['minHeight']]
);
$code = 1715964043;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if ((int)$this->options['maxWidth'] < $imageInfo->getWidth()) {
$message = $this->translateErrorMessage(
$this->maxWidthMessage,
'',
[$this->options['maxWidth']]
);
$code = 1715964044;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if ((int)$this->options['maxHeight'] < $imageInfo->getHeight()) {
$message = $this->translateErrorMessage(
$this->maxHeightMessage,
'',
[$this->options['maxHeight']]
);
$code = 1715964045;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
private function getImageInfo(string $filePath): ImageInfo
{
return GeneralUtility::makeInstance(ImageInfo::class, $filePath);
}
/**
* Checks if this validator is correctly configured
*/
private function validateOptions(): void
{
if ((int)$this->options['minWidth'] > (int)$this->options['maxWidth']) {
throw new InvalidValidationOptionsException('The option "minWidth" must not be greater than "maxWidth"', 1716008127);
}
if ((int)$this->options['minHeight'] > (int)$this->options['maxHeight']) {
throw new InvalidValidationOptionsException('The option "minHeight" must not be greater than "maxHeight"', 1716008128);
}
}
}
@@ -0,0 +1,40 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for integers.
*/
final class IntegerValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.integer.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid integer.
*/
public function isValid(mixed $value): void
{
if (filter_var($value, FILTER_VALIDATE_INT) === false) {
$this->addError($this->translateErrorMessage($this->message), 1221560494);
}
}
}
@@ -0,0 +1,115 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Http\UploadedFile;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for mime types of a given UploadedFile or ObjectStorage of UploadedFile objects. Does also validate, if
* the given file extension matches allowed file extensions for the detected mime type.
*/
final class MimeTypeValidator extends AbstractValidator
{
protected string $notAllowedMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.mimetype.notallowed';
protected string $invalidExtensionMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validation.mimetype.invalidextension';
protected array $translationOptions = ['notAllowedMessage', 'invalidExtensionMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'allowedMimeTypes' => [null, 'Allowed mime types (using */* IANA media types)', 'array', true],
'ignoreFileExtensionCheck' => [false, 'If set to "true", the file extension check is disabled. Be aware of security considerations when setting this to "true".', 'boolean'],
'notAllowedMessage' => [null, 'Translation key or message for not allowed MIME type', 'string'],
'invalidExtensionMessage' => [null, 'Translation key or message for invalid file extension', 'string'],
];
public function isValid(mixed $value): void
{
$this->ensureFileUploadTypes($value);
$this->validateOptions();
if ($value instanceof UploadedFile) {
$this->validateUploadedFile($value);
} elseif ($value instanceof ObjectStorage && $value->count() > 0) {
$index = 0;
foreach ($value as $uploadedFile) {
$this->validateUploadedFile($uploadedFile, $index);
$index++;
}
}
}
private function validateUploadedFile(UploadedFile $uploadedFile, ?int $index = null): void
{
$fileInfo = $this->getFileInfo($uploadedFile->getTemporaryFileName());
$mimeType = $fileInfo->getMimeType();
$allowedMimeTypes = $this->options['allowedMimeTypes'];
$ignoreFileExtensionCheck = $this->options['ignoreFileExtensionCheck'];
if (!in_array($mimeType, $allowedMimeTypes, true)) {
$message = $this->translateErrorMessage(
$this->notAllowedMessage,
'',
[$mimeType]
);
$code = 1708538973;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
if (!$ignoreFileExtensionCheck && !$this->result->hasErrors()) {
// The file extension of the uploaded file must match the mime-type for this file.
// Example: myfile.txt is actually a PDF file (defined by mime-type), but .txt is not associated
// for application/pdf, so this is not valid.
$fileExtension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
$assumedMimesTypeOfFileExtension = (new MimeTypeDetector())->getMimeTypesForFileExtension($fileExtension);
if (empty(array_intersect($allowedMimeTypes, $assumedMimesTypeOfFileExtension))) {
$message = $this->translateErrorMessage(
$this->invalidExtensionMessage,
'',
[$fileExtension]
);
$code = 1718469466;
if ($index !== null) {
$this->addErrorForProperty((string)$index, $message, $code);
} else {
$this->addError($message, $code);
}
}
}
}
/**
* Checks if this validator is correctly configured
*/
private function validateOptions(): void
{
if (!is_array($this->options['allowedMimeTypes'] ?? false) || $this->options['allowedMimeTypes'] === []) {
throw new InvalidValidationOptionsException('The option "allowedMimeTypes" must be an array with at least one item.', 1708526223);
}
}
}
@@ -0,0 +1,61 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for not empty values.
*/
final class NotEmptyValidator extends AbstractValidator
{
/**
* This validator always needs to be executed even if the given value is empty.
* See AbstractValidator::validate()
*
* @var bool
*/
protected $acceptsEmptyValues = false;
protected string $nullMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.notempty.null';
protected string $emptyMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.notempty.empty';
protected array $translationOptions = ['nullMessage', 'emptyMessage'];
protected $supportedOptions = [
'nullMessage' => [null, 'Translation key or message for null value', 'string'],
'emptyMessage' => [null, 'Translation key or message for empty value', 'string'],
];
/**
* Checks if the given value ($propertyValue) is not empty (NULL, empty string, empty array or empty object).
*/
public function isValid(mixed $value): void
{
if ($value === null) {
$this->addError($this->translateErrorMessage($this->nullMessage), 1221560910);
}
if ($value === '') {
$this->addError($this->translateErrorMessage($this->emptyMessage), 1221560718);
}
if (is_array($value) && empty($value)) {
$this->addError($this->translateErrorMessage($this->emptyMessage), 1347992400);
}
if ($value instanceof \Countable && $value->count() === 0) {
$this->addError($this->translateErrorMessage($this->emptyMessage), 1347992453);
}
}
}
@@ -0,0 +1,69 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for general numbers
*/
final class NumberRangeValidator extends AbstractValidator
{
protected string $notValidMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.numberrange.notvalid';
protected string $notInRangeMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.numberrange.range';
protected array $translationOptions = ['notValidMessage', 'notInRangeMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'minimum' => [0, 'The minimum value to accept', 'integer'],
'maximum' => [PHP_INT_MAX, 'The maximum value to accept', 'integer'],
'notValidMessage' => [null, 'Translation key or message for non valid value', 'string'],
'notInRangeMessage' => [null, 'Translation key or message for value not in range', 'string'],
];
/**
* The given value is valid if it is a number in the specified range.
*/
public function isValid(mixed $value): void
{
if (!is_numeric($value)) {
$this->addError($this->translateErrorMessage($this->notValidMessage), 1221563685);
return;
}
$minimum = $this->options['minimum'];
$maximum = $this->options['maximum'];
if ($minimum > $maximum) {
$x = $minimum;
$minimum = $maximum;
$maximum = $x;
}
if ($value < $minimum || $value > $maximum) {
$this->addError($this->translateErrorMessage(
$this->notInRangeMessage,
'',
[
$minimum,
$maximum,
]
), 1221561046, [$minimum, $maximum]);
}
}
}
@@ -0,0 +1,40 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for general numbers.
*/
final class NumberValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.number.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid number.
*/
public function isValid(mixed $value): void
{
if (!is_numeric($value)) {
$this->addError($this->translateErrorMessage($this->message), 1221563685);
}
}
}
@@ -0,0 +1,31 @@
<?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\Extbase\Validation\Validator;
/**
* Contract for a validator
*/
interface ObjectValidatorInterface extends ValidatorInterface
{
/**
* Allows to set a container to keep track of validated instances.
*
* @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances
*/
public function setValidatedInstancesContainer(\SplObjectStorage $validatedInstancesContainer): void;
}
@@ -0,0 +1,55 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator based on regular expressions.
*/
final class RegularExpressionValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.regularexpression.nomatch';
/**
* @var array
*/
protected $supportedOptions = [
'regularExpression' => ['', 'The regular expression to use for validation, used as given', 'string', true],
'message' => [null, 'Translation key or message when regular expression results in a no match', 'string'],
];
/**
* Checks if the given value matches the specified regular expression.
*
* @throws InvalidValidationOptionsException
*/
public function isValid(mixed $value): void
{
$result = preg_match($this->options['regularExpression'], $value);
if ($result === 0) {
$this->addError(
$this->translateErrorMessage($this->message),
1221565130
);
}
if ($result === false) {
throw new InvalidValidationOptionsException('regularExpression "' . $this->options['regularExpression'] . '" in RegularExpressionValidator contained an error.', 1298273089);
}
}
}
@@ -0,0 +1,118 @@
<?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\Extbase\Validation\Validator;
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for string length.
*/
final class StringLengthValidator extends AbstractValidator
{
protected string $betweenMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.stringlength.between';
protected string $lessMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.stringlength.less';
protected string $exceedMessage = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.stringlength.exceed';
protected array $translationOptions = ['betweenMessage', 'lessMessage', 'exceedMessage'];
/**
* @var array
*/
protected $supportedOptions = [
'minimum' => [0, 'Minimum length for a valid string', 'integer'],
'maximum' => [PHP_INT_MAX, 'Maximum length for a valid string', 'integer'],
'betweenMessage' => [null, 'Translation key or message for value not between minimum and maximum', 'string'],
'lessMessage' => [null, 'Translation key or message for value less than minimum', 'string'],
'exceedMessage' => [null, 'Translation key or message for value exceeds maximum', 'string'],
];
/**
* Checks if the given value is a valid string (or can be cast to a string
* if an object is given) and its length is between minimum and maximum
* specified in the validation options.
*
* @throws InvalidValidationOptionsException
*/
public function isValid(mixed $value): void
{
if ($this->options['maximum'] < $this->options['minimum']) {
throw new InvalidValidationOptionsException('The \'maximum\' is shorter than the \'minimum\' in the StringLengthValidator.', 1238107096);
}
if (is_object($value)) {
if (!method_exists($value, '__toString')) {
$this->addError('The given object could not be converted to a string.', 1238110957);
return;
}
} elseif (!is_string($value)) {
$this->addError('The given value was not a valid string.', 1269883975);
return;
}
$value = (string)$value;
$stringLength = mb_strlen($value, 'utf-8');
$isValid = true;
if ($stringLength < $this->options['minimum']) {
$isValid = false;
}
if ($stringLength > $this->options['maximum']) {
$isValid = false;
}
if ($isValid === false) {
if ($this->options['minimum'] > 0 && $this->options['maximum'] < PHP_INT_MAX) {
$this->addError(
$this->translateErrorMessage(
$this->betweenMessage,
'',
[
$this->options['minimum'],
$this->options['maximum'],
]
),
1428504122,
[$this->options['minimum'], $this->options['maximum']]
);
} elseif ($this->options['minimum'] > 0) {
$this->addError(
$this->translateErrorMessage(
$this->lessMessage,
'',
[
$this->options['minimum'],
]
),
1238108068,
[$this->options['minimum']]
);
} else {
$this->addError(
$this->translateErrorMessage(
$this->exceedMessage,
'',
[
$this->options['maximum'],
]
),
1238108069,
[$this->options['maximum']]
);
}
}
}
}
@@ -0,0 +1,40 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for strings.
*/
final class StringValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.string.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a string.
*/
public function isValid(mixed $value): void
{
if (!is_string($value)) {
$this->addError($this->translateErrorMessage($this->message), 1238108067);
}
}
}
@@ -0,0 +1,44 @@
<?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\Extbase\Validation\Validator;
/**
* Validator for "plain" text.
*/
final class TextValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.text.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid text (contains no XML tags).
*
* Be aware that the value of this check entirely depends on the output context.
* The validated text is not expected to be secure in every circumstance, if you
* want to be sure of that, use a customized regular expression or filter on output.
*/
public function isValid(mixed $value): void
{
if ($value !== strip_tags((string)$value)) {
$this->addError($this->translateErrorMessage($this->message), 1221565786);
}
}
}
@@ -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\Extbase\Validation\Validator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Validator for url.
*/
final class UrlValidator extends AbstractValidator
{
protected string $message = 'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:validator.url.notvalid';
protected $supportedOptions = [
'message' => [null, 'Translation key or message for invalid value', 'string'],
];
/**
* Checks if the given value is a valid url.
*/
public function isValid(mixed $value): void
{
if (!is_string($value) || !GeneralUtility::isValidUrl($value)) {
$this->addError($this->translateErrorMessage($this->message), 1238108078);
}
}
}
@@ -0,0 +1,55 @@
<?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\Extbase\Validation\Validator;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Extbase\Error\Result;
/**
* Contract for a validator
*/
interface ValidatorInterface
{
/**
* Checks if the given value is valid according to the validator, and returns
* the Error Messages object which occurred.
*
* @param mixed $value The value that should be validated
*/
public function validate(mixed $value): Result;
/**
* Receive validator options from framework.
*/
public function setOptions(array $options): void;
/**
* Returns the options of this validator which can be specified by setOptions().
*/
public function getOptions(): array;
/**
* Sets the request object to the validator.
*/
public function setRequest(?ServerRequestInterface $request): void;
/**
* Returns the request object of the validator.
*/
public function getRequest(): ?ServerRequestInterface;
}
@@ -0,0 +1,80 @@
<?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\Extbase\Validation;
use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
final class ValidatorClassNameResolver
{
/**
* This method is marked internal due to several facts:
*
* - The functionality is not 100% tested and still contains some bugs
* - The functionality might not be needed any longer if Extbase switches
* to the symfony/validator component.
*
* This method can be used by extension developers. As long as it remains,
* its functionality will not change. It might even become more stable.
* However, developers should be aware that this method might vanish
* without any deprecation.
*
* @throws NoSuchValidatorException
* @internal
*/
public static function resolve(string $validatorIdentifier): string
{
// Trim leading slash if $validatorName is FQCN like \TYPO3\CMS\Extbase\Validation\Validator\FloatValidator
$validatorIdentifier = ltrim($validatorIdentifier, '\\');
$validatorClassName = $validatorIdentifier;
if (strpbrk($validatorIdentifier, '\\') === false) {
// Shorthand built in
$validatorClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . self::getValidatorType($validatorIdentifier);
}
if (!str_ends_with($validatorClassName, 'Validator')) {
$validatorClassName .= 'Validator';
}
if (!class_exists($validatorClassName)) {
throw new NoSuchValidatorException('Validator class ' . $validatorClassName . ' does not exist', 1365799920);
}
if (!is_subclass_of($validatorClassName, ValidatorInterface::class)) {
throw new NoSuchValidatorException(
'Validator class ' . $validatorClassName . ' must implement ' . ValidatorInterface::class,
1365776838
);
}
return $validatorClassName;
}
/**
* Used to map PHP types to validator types.
*
* @param string $type Data type to unify
* @return string unified data type
*/
private static function getValidatorType(string $type): string
{
return match ($type) {
'int' => 'Integer',
'bool' => 'Boolean',
'double' => 'Float',
'numeric' => 'Number',
default => ucfirst($type),
};
}
}
+207
View File
@@ -0,0 +1,207 @@
<?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\Extbase\Validation;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\TypeAdapter;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
use TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator;
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
use TYPO3\CMS\Extbase\Validation\Validator\ConstraintDecoratingValidator;
use TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator;
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
/**
* Validator resolver to automatically find an appropriate validator for a given subject.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class ValidatorResolver implements SingletonInterface
{
protected array $baseValidatorConjunctions = [];
public function __construct(protected readonly ReflectionService $reflectionService) {}
/**
* Get a validator for a given data type. Returns a validator implementing
* the ValidatorInterface or NULL if no validator could be resolved.
*
* @param string $validatorType Either one of the built-in data types or fully qualified validator class name
*/
public function createValidator(
string $validatorType,
array $validatorOptions = [],
?ServerRequestInterface $request = null
): ?ValidatorInterface {
try {
$validatorObjectName = ValidatorClassNameResolver::resolve($validatorType);
/** @var ValidatorInterface $validator */
$validator = GeneralUtility::makeInstance($validatorObjectName);
$validator->setOptions($validatorOptions);
$validator->setRequest($request);
return $validator;
} catch (NoSuchValidatorException $e) {
GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__)->debug($e->getMessage());
return null;
}
}
/**
* Resolves and returns the base validator conjunction for the given data type.
* If no validator could be resolved (which usually means that no validation is necessary), NULL is returned.
*
* @param string $targetClassName The data type to search a validator for. Usually the fully qualified object name
*/
public function getBaseValidatorConjunction(
string $targetClassName,
?ServerRequestInterface $request = null
): ConjunctionValidator {
if (!isset($this->baseValidatorConjunctions[$targetClassName])) {
$conjunctionValidator = GeneralUtility::makeInstance(ConjunctionValidator::class);
$this->baseValidatorConjunctions[$targetClassName] = $conjunctionValidator;
// The simpleType check reduces lookups to the class loader
if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
$this->buildBaseValidatorConjunction($conjunctionValidator, $targetClassName, $request);
}
}
return $this->baseValidatorConjunctions[$targetClassName];
}
/**
* Builds a base validator conjunction for the given data type.
*
* The base validation rules are those which were declared directly in a class (typically
* a model) through some #[Validate] attributes on properties.
*
* If a property holds a class for which a base validator exists, that property will be
* checked as well, regardless of a validation attribute.
*
* Additionally, if a custom validator was defined for the class in question, it will be added
* to the end of the conjunction. A custom validator is found if it follows the naming convention
* "Replace '\Model\' by '\Validator\' and append 'Validator'".
*
* Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the
* name TYPO3\Foo\Domain\Validator\QuuxValidator
*
* @param class-string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name.
* @throws NoSuchValidatorException
* @throws \InvalidArgumentException
*/
protected function buildBaseValidatorConjunction(
ConjunctionValidator $conjunctionValidator,
string $targetClassName,
?ServerRequestInterface $request
): void {
$classSchema = $this->reflectionService->getClassSchema($targetClassName);
// Model based validator
/** @var GenericObjectValidator $objectValidator */
$objectValidator = $this->createValidator(GenericObjectValidator::class);
foreach ($classSchema->getProperties() as $property) {
$primaryType = $property->getPrimaryType();
if (!$primaryType instanceof TypeAdapter) {
// @todo: The type is only necessary here for further analyzing whether it's a simple type or
// a collection. If this is evaluated in the ClassSchema, this whole code part is not needed
// any longer and can be removed.
throw new \InvalidArgumentException(
sprintf('There is no @var annotation or type declaration for property "%s" in class "%s".', $property->getName(), $targetClassName),
1363778104
);
}
$propertyTargetClassName = $primaryType->getClassName() ?? $primaryType->getBuiltinType();
// Skip transient properties for auto-generated validators (model-typed properties).
// Transient properties are not persisted and may not have public accessors.
if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName) && !$property->isTransient()) {
// The outer simpleType check reduces lookups to the class loader
// @todo: Whether the property holds a simple type or not and whether it holds a collection is known in
// in the ClassSchema. The information could be made available and not evaluated here again.
$primaryCollectionValueType = $property->getPrimaryCollectionValueType();
if ($primaryType->isCollection() && $primaryCollectionValueType instanceof TypeAdapter) {
/** @var CollectionValidator $collectionValidator */
$collectionValidator = $this->createValidator(
CollectionValidator::class,
[
'elementType' => $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType(),
],
$request
);
$objectValidator->addPropertyValidator($property->getName(), $collectionValidator);
} elseif (class_exists($propertyTargetClassName)
&& !TypeHandlingUtility::isCoreType($propertyTargetClassName)
&& !in_array(SingletonInterface::class, class_implements($propertyTargetClassName) ?: [], true)
) {
// class_exists($propertyTargetClassName) checks, if the type of the property is an object
// instead of a simple type. Like DateTime or another model.
//
// !TypeHandlingUtility::isCoreType($propertyTargetClassName) checks if the type of the property
// is not a core type, which are Enums and File objects for example.
// @todo: check why these types shouldn't be validated.
//
// !in_array(SingletonInterface::class, class_implements($propertyTargetClassName, true), true)
// checks if the class is an instance of a Singleton
// @todo: check why Singletons shouldn't be validated.
//
// (Alexander Schnitzler) By looking at this code I assume that this is the path for 1:1
// relations in models. Still, the question remains why it excludes core types and singletons.
// It makes sense on a theoretical level, but I don't see a technical issue allowing these as
// well.
$validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName, $request);
if ($validatorForProperty->count() > 0) {
$objectValidator->addPropertyValidator($property->getName(), $validatorForProperty);
}
}
}
foreach ($property->getValidators() as $validatorDefinition) {
// @todo: At this point we already have the class name of the validator, thus there is not need
// calling ValidatorClassNameResolver::resolve inside
// \TYPO3\CMS\Extbase\Validation\ValidatorResolver::createValidator once again. However, to
// keep things simple for now, we still use the method createValidator here. In the future,
// createValidator must only accept FQCN's.
if (isset($validatorDefinition['constraint'])) {
$newValidator = new ConstraintDecoratingValidator($validatorDefinition['constraint']);
} else {
$newValidator = $this->createValidator(
$validatorDefinition['className'],
$validatorDefinition['options'],
$request,
);
}
if ($newValidator === null) {
throw new NoSuchValidatorException(
'Invalid #[Validate] attribute in ' . $targetClassName . '::' . $property->getName() . ': '
. 'Could not resolve class name for validator "' . $validatorDefinition['className'] . '".',
1241098027
);
}
$objectValidator->addPropertyValidator($property->getName(), $newValidator);
}
}
if (!empty($objectValidator->getPropertyValidators())) {
$conjunctionValidator->addValidator($objectValidator);
}
}
}