Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add strict component argument validation #131

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Classes/Exception/InvalidTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace SMS\FluidComponents\Exception;

class InvalidTypeException extends \Exception
{
}
46 changes: 44 additions & 2 deletions Classes/Fluid/ViewHelper/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Psr\Container\ContainerInterface;
use SMS\FluidComponents\Domain\Model\Slot;
use SMS\FluidComponents\Exception\InvalidTypeException;
use SMS\FluidComponents\Interfaces\ComponentAware;
use SMS\FluidComponents\Interfaces\EscapedParameter;
use SMS\FluidComponents\Interfaces\RenderingContextAware;
Expand Down Expand Up @@ -45,6 +46,13 @@ class ComponentRenderer extends AbstractViewHelper
*/
protected $componentNamespace;

/**
* Whether strict argument validation is enabled
*
* @var bool
*/
protected $componentNamespaceIsStrict = false;

/**
* Cache for component template instance used for rendering
*
Expand Down Expand Up @@ -130,6 +138,12 @@ public function __construct(
public function setComponentNamespace($componentNamespace)
{
$this->componentNamespace = $componentNamespace;
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['strict'] ?? [] as $strictNamespace) {
if (str_starts_with($componentNamespace, $strictNamespace)) {
$this->componentNamespaceIsStrict = true;
break;
}
}
return $this;
}

Expand Down Expand Up @@ -200,8 +214,28 @@ public function render()
// Provide supplied arguments from component call to renderer
foreach ($this->arguments as $name => $argument) {
$argumentType = $this->argumentDefinitions[$name]->getType();

$argument = $this->componentArgumentConverter->convertValueToType($argument, $argumentType);
$argumentIsRequired = $this->argumentDefinitions[$name]->isRequired();
$argumentDefaultValue = $this->argumentDefinitions[$name]->getDefaultValue();
if (!$argumentIsRequired) {
$argument ??= $argumentDefaultValue;
}
try {
$argument = $this->componentArgumentConverter->convertValueToType($argument, $argumentType);
} catch (InvalidTypeException $e) {
if ($this->componentNamespaceIsStrict && ($argumentIsRequired || $argument !== $this->argumentDefinitions[$name]->getDefaultValue())) {
throw new Exception(
sprintf(
'The argument "%s" defined in "%s" has declared type "%s", but "%s" was given.',
$name,
$this->getComponentNamespace(),
$argumentType,
is_object($argument) ? get_class($argument) : gettype($argument)
),
1677141778,
$e
);
}
}

// Provide component namespace to certain data structures
if ($argument instanceof ComponentAware) {
Expand Down Expand Up @@ -428,6 +462,14 @@ protected function initializeComponentParams()
foreach ($paramNode->getArguments() as $argumentName => $argumentNode) {
$param[$argumentName] = $argumentNode->evaluate($renderingContext);
}
if ($this->componentNamespaceIsStrict && !$this->componentArgumentConverter->isValidType($param['type'])) {
throw new Exception(sprintf(
'The argument "%s" defined in "%s" has an invalid type declaration "%s".',
$param['name'],
$this->getComponentNamespace(),
$param['type']
), 1677141778);
}

// Use tag content as default value instead of attribute
if (!isset($param['default'])) {
Expand Down
29 changes: 29 additions & 0 deletions Classes/Utility/ComponentArgumentConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace SMS\FluidComponents\Utility;

use SMS\FluidComponents\Exception\InvalidTypeException;
use SMS\FluidComponents\Interfaces\ConstructibleFromArray;
use SMS\FluidComponents\Interfaces\ConstructibleFromDateTime;
use SMS\FluidComponents\Interfaces\ConstructibleFromDateTimeImmutable;
Expand All @@ -16,6 +17,14 @@

class ComponentArgumentConverter implements \TYPO3\CMS\Core\SingletonInterface
{
private const NATIVE_TYPES = [
'integer',
'float',
'string',
'boolean',
'array',
];

/**
* List of interfaces that provide conversion methods between scalar/compound
* variable types and complex data structures,
Expand Down Expand Up @@ -217,6 +226,14 @@ public function convertValueToType($value, string $toType)

// Skip if the type can't be converted
if (!$this->canTypeBeConvertedToType($givenType, $toType)) {
if ($givenType !== $toType && !is_a($value, $toType)) {
throw new InvalidTypeException(sprintf(
'The argument value is of type "%s", but "%s" is given.',
$toType,
$givenType
), 1677142681);

}
return $value;
}

Expand All @@ -234,6 +251,18 @@ public function convertValueToType($value, string $toType)
return $toType::$constructor($value);
}

public function isValidType(string $type): bool
{
if (in_array($type, self::NATIVE_TYPES, true)) {
return true;
}
$type = $this->resolveTypeAlias($type);
if ($this->isCollectionType($type)) {
return $this->isValidType($this->extractCollectionItemType($type));
}
return class_exists($type);
}

/**
* Checks if the provided type describes a collection of values
*
Expand Down