first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,202 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager as ExtbaseConfigurationManager;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\ExtensionNameRequiredException;
/**
* Extend the ExtbaseConfigurationManager to read YAML configurations.
*
* Scope: frontend / backend
* @internal
*/
class ConfigurationManager extends ExtbaseConfigurationManager implements ConfigurationManagerInterface
{
/**
* @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
*/
protected $cache;
/**
* @var \TYPO3\CMS\Form\Mvc\Configuration\YamlSource
*/
protected $yamlSource;
/**
* @param \TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource
* @internal
*/
public function injectYamlSource(\TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource)
{
$this->yamlSource = $yamlSource;
}
/**
* @param string $configurationType The kind of configuration to fetch - must be one of the CONFIGURATION_TYPE_* constants
* @param string $extensionName if specified, the configuration for the given extension will be returned.
* @param string $pluginName if specified, the configuration for the given plugin will be returned.
* @return array The configuration
* @internal
*/
public function getConfiguration($configurationType, $extensionName = null, $pluginName = null)
{
switch ($configurationType) {
case self::CONFIGURATION_TYPE_YAML_SETTINGS:
return $this->getConfigurationFromYamlFile($extensionName);
default:
return parent::getConfiguration($configurationType, $extensionName, $pluginName);
}
}
/**
* Load and parse YAML files which are configured within the TypoScript
* path plugin.tx_extensionkey.settings.yamlConfigurations
*
* The following steps will be done:
*
* * Convert each singe YAML file into an array
* * merge this arrays together
* * resolve all declared inheritances
* * remove all keys if their values are NULL
* * return all configuration paths within TYPO3.CMS
* * sort by array keys, if all keys within the current nesting level are numerical keys
* * resolve possible TypoScript settings in FE mode
*
* @param string $extensionName
* @return array
* @throws ExtensionNameRequiredException
*/
protected function getConfigurationFromYamlFile(string $extensionName): array
{
if (empty($extensionName)) {
throw new ExtensionNameRequiredException(
'Please specify an extension key to load a YAML configuration',
1471473377
);
}
$ucFirstExtensioName = ucfirst($extensionName);
$typoscriptSettings = $this->getTypoScriptSettings($extensionName);
$yamlSettingsFilePaths = isset($typoscriptSettings['yamlConfigurations'])
? ArrayUtility::sortArrayWithIntegerKeys($typoscriptSettings['yamlConfigurations'])
: [];
$cacheKeySuffix = $extensionName . md5(json_encode($yamlSettingsFilePaths));
$yamlSettings = $this->getYamlSettingsFromCache($cacheKeySuffix);
if (!empty($yamlSettings)) {
return $this->overrideConfigurationByTypoScript($yamlSettings, $extensionName);
}
$yamlSettings = InheritancesResolverService::create($this->yamlSource->load($yamlSettingsFilePaths))
->getResolvedConfiguration();
$yamlSettings = ArrayUtility::removeNullValuesRecursive($yamlSettings);
$yamlSettings = is_array($yamlSettings['TYPO3']['CMS'][$ucFirstExtensioName])
? $yamlSettings['TYPO3']['CMS'][$ucFirstExtensioName]
: [];
$yamlSettings = ArrayUtility::sortArrayWithIntegerKeysRecursive($yamlSettings);
$this->setYamlSettingsIntoCache($cacheKeySuffix, $yamlSettings);
return $this->overrideConfigurationByTypoScript($yamlSettings, $extensionName);
}
/**
* @param array $yamlSettings
* @param string $extensionName
* @return array
*/
protected function overrideConfigurationByTypoScript(array $yamlSettings, string $extensionName): array
{
$typoScript = parent::getConfiguration(self::CONFIGURATION_TYPE_SETTINGS, $extensionName);
if (is_array($typoScript['yamlSettingsOverrides']) && !empty($typoScript['yamlSettingsOverrides'])) {
ArrayUtility::mergeRecursiveWithOverrule(
$yamlSettings,
$typoScript['yamlSettingsOverrides']
);
if ($this->environmentService->isEnvironmentInFrontendMode()) {
$yamlSettings = $this->objectManager->get(TypoScriptService::class)
->resolvePossibleTypoScriptConfiguration($yamlSettings);
}
}
return $yamlSettings;
}
/**
* @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
*/
protected function getCacheFrontend(): FrontendInterface
{
if ($this->cache === null) {
$this->cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
}
return $this->cache;
}
/**
* @param string $cacheKeySuffix
* @return string
*/
protected function getConfigurationCacheKey(string $cacheKeySuffix): string
{
return strtolower(self::CONFIGURATION_TYPE_YAML_SETTINGS . '_' . $cacheKeySuffix);
}
/**
* @param string $cacheKeySuffix
* @return mixed
*/
protected function getYamlSettingsFromCache(string $cacheKeySuffix)
{
return $this->getCacheFrontend()->get(
$this->getConfigurationCacheKey($cacheKeySuffix)
);
}
/**
* @param string $cacheKeySuffix
* @param array $yamlSettings
*/
protected function setYamlSettingsIntoCache(
string $cacheKeySuffix,
array $yamlSettings
) {
$this->getCacheFrontend()->set(
$this->getConfigurationCacheKey($cacheKeySuffix),
$yamlSettings
);
}
/**
* @param string $extensionName
* @return null|[]
*/
protected function getTypoScriptSettings(string $extensionName)
{
return parent::getConfiguration(
ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS,
$extensionName
);
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
/**
* Class ConfigurationManagerInterface
*
* Scope: frontend / backend
* @internal
*/
interface ConfigurationManagerInterface extends ExtbaseConfigurationManagerInterface
{
const CONFIGURATION_TYPE_YAML_SETTINGS = 'YamlSettings';
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Exception as FormException;
/**
* A generic Form configuration Exception
*
* @internal
*/
class Exception extends FormException
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* This exception is thrown if the InheritancesResolverService wants to
* solve declared inheritances which point cyclically to themselves.
*
* @internal
*/
class CycleInheritancesException extends Exception
{
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* This exception is thrown if the configuration manager wants to load
* a YAML file from an empty extension key
*
* @internal
*/
class ExtensionNameRequiredException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
namespace TYPO3\CMS\Form\Mvc\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* Exception for file write errors
* @internal
*/
class FileWriteException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
namespace TYPO3\CMS\Form\Mvc\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* A No Such File exception
* @internal
*/
class NoSuchFileException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
namespace TYPO3\CMS\Form\Mvc\Configuration\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Configuration\Exception;
/**
* A Parse Error exception
* @internal
*/
class ParseErrorException extends Exception
{
}
@@ -0,0 +1,362 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\CycleInheritancesException;
/**
* Resolve declared inheritances within an configuration array
*
* Basic concept:
* - Take a large YAML config and replace the key '__inheritance' by the referenced YAML partial (of the same config file)
* - Maybe also override some keys of the referenced partial
* - Avoid endless loop by reference cycles
*
* e.g.
* ---------------------
*
* Form:
* part1:
* key1: value1
* key2: value2
* key3: value3
* part2:
* __inheritance:
* 10: Form.part1
* key2: another_value
*
* will result in:
* ---------------------
*
* Form:
* part1:
* key1: value1
* key2: value2
* key3: value3
* part2:
* key1: value1
* key2: another_value
* key3: value3
*
* ---------------------
* Scope: frontend / backend
* @internal
*/
class InheritancesResolverService
{
/**
* The operator which is used to declare inheritances
*/
const INHERITANCE_OPERATOR = '__inheritances';
/**
* The reference configuration is used to get untouched values which
* can be merged into the touched configuration.
*
* @var array
*/
protected $referenceConfiguration = [];
/**
* This stack is needed to find cyclically inheritances which are on
* the same nesting level but which do not follow each other directly.
*
* @var array
*/
protected $inheritanceStack = [];
/**
* Needed to buffer a configuration path for cyclically inheritances
* detection while inheritances for this path is ongoing.
*
* @var string
*/
protected $inheritancePathToCheck = '';
/**
* Returns an instance of this service. Additionally the configuration
* which should be resolved can be passed.
*
* @param array $configuration
* @return InheritancesResolverService
* @internal
*/
public static function create(array $configuration = []): InheritancesResolverService
{
/** @var InheritancesResolverService $inheritancesResolverService */
$inheritancesResolverService = GeneralUtility::makeInstance(ObjectManager::class)
->get(self::class);
$inheritancesResolverService->setReferenceConfiguration($configuration);
return $inheritancesResolverService;
}
/**
* Reset the state of this service.
* Mainly introduced for unit tests.
*
* @return InheritancesResolverService
* @internal
*/
public function reset()
{
$this->referenceConfiguration = [];
$this->inheritanceStack = [];
$this->inheritancePathToCheck = '';
return $this;
}
/**
* Set the reference configuration which is used to get untouched
* values which can be merged into the touched configuration.
*
* @param array $referenceConfiguration
* @return InheritancesResolverService
*/
public function setReferenceConfiguration(array $referenceConfiguration)
{
$this->referenceConfiguration = $referenceConfiguration;
return $this;
}
/**
* Resolve all inheritances within a configuration.
* After that the configuration array is cleaned from the
* inheritance operator.
*
* @return array
* @internal
*/
public function getResolvedConfiguration(): array
{
$configuration = $this->resolve($this->referenceConfiguration);
$configuration = $this->removeInheritanceOperatorRecursive($configuration);
return $configuration;
}
/**
* Resolve all inheritances within a configuration.
*
* Takes a YAML config mapped to associative array $configuration
* - replace all findings of key '__inheritance' recursively
* - perform a deep search in config by iteration, thus check for endless loop by reference cycle
*
* Return the completed configuration.
*
* @param array $configuration - a mapped YAML configuration (full or partial)
* @param array $pathStack - an identifier for YAML key as array (Form.part1.key => {Form, part1, key})
* @param bool $setInheritancePathToCheck
* @return array
*/
protected function resolve(
array $configuration,
array $pathStack = [],
bool $setInheritancePathToCheck = true
): array {
foreach ($configuration as $key => $values) {
//add current key to pathStack
$pathStack[] = $key;
$path = implode('.', $pathStack);
//check endless loop for current path
$this->throwExceptionIfCycleInheritances($path, $path);
//overwrite service property 'inheritancePathToCheck' with current path
if ($setInheritancePathToCheck) {
$this->inheritancePathToCheck = $path;
}
//if value of subnode is an array, perform a deep search iteration step
if (is_array($configuration[$key])) {
if (isset($configuration[$key][self::INHERITANCE_OPERATOR])) {
$inheritances = $this->getValueByPath($this->referenceConfiguration, $path . '.' . self::INHERITANCE_OPERATOR);
//and replace the __inheritance operator by the respective partial
if (is_array($inheritances)) {
$inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
$configuration[$key] = array_replace_recursive($inheritedConfigurations, $configuration[$key]);
}
//remove the inheritance operator from configuration
unset($configuration[$key][self::INHERITANCE_OPERATOR]);
}
if (!empty($configuration[$key])) {
// resolve subnode of YAML config
$configuration[$key] = $this->resolve($configuration[$key], $pathStack);
}
}
array_pop($pathStack);
}
return $configuration;
}
/**
* Additional helper for the resolve method.
*
* Takes all inheritances (an array of YAML paths), and check them for endless loops
*
* @param array $inheritances
* @return array
* @throws CycleInheritancesException
*/
protected function resolveInheritancesRecursive(array $inheritances): array
{
ksort($inheritances);
$inheritedConfigurations = [];
foreach ($inheritances as $inheritancePath) {
$this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
$inheritedConfiguration = $this->getValueByPath($this->referenceConfiguration, $inheritancePath);
if (
isset($inheritedConfiguration[self::INHERITANCE_OPERATOR])
&& count($inheritedConfiguration) === 1
) {
if ($this->inheritancePathToCheck === $inheritancePath) {
throw new CycleInheritancesException(
$this->inheritancePathToCheck . ' has cycle inheritances',
1474900796
);
}
$inheritedConfiguration = $this->resolveInheritancesRecursive(
$inheritedConfiguration[self::INHERITANCE_OPERATOR]
);
} else {
$pathStack = explode('.', $inheritancePath);
$key = array_pop($pathStack);
$newConfiguration = [
$key => $inheritedConfiguration
];
$inheritedConfiguration = $this->resolve(
$newConfiguration,
$pathStack,
false
);
$inheritedConfiguration = $inheritedConfiguration[$key];
}
if ($inheritedConfiguration === null) {
throw new CycleInheritancesException(
$inheritancePath . ' does not exist within the configuration',
1489260796
);
}
$inheritedConfigurations = array_replace_recursive(
$inheritedConfigurations,
$inheritedConfiguration
);
}
return $inheritedConfigurations;
}
/**
* Throw an exception if a cycle is detected.
*
* @param string $path
* @param string $pathToCheck
* @throws CycleInheritancesException
*/
protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
{
$configuration = $this->getValueByPath($this->referenceConfiguration, $path);
if (isset($configuration[self::INHERITANCE_OPERATOR])) {
$inheritances = $this->getValueByPath($this->referenceConfiguration, $path . '.' . self::INHERITANCE_OPERATOR);
if (is_array($inheritances)) {
foreach ($inheritances as $inheritancePath) {
$configuration = $this->getValueByPath($this->referenceConfiguration, $inheritancePath);
if (isset($configuration[self::INHERITANCE_OPERATOR])) {
$_inheritances = $this->getValueByPath($this->referenceConfiguration, $inheritancePath . '.' . self::INHERITANCE_OPERATOR);
foreach ($_inheritances as $_inheritancePath) {
if (strpos($pathToCheck, $_inheritancePath) === 0) {
throw new CycleInheritancesException(
$pathToCheck . ' has cycle inheritances',
1474900797
);
}
}
}
if (
isset($this->inheritanceStack[$pathToCheck])
&& is_array($this->inheritanceStack[$pathToCheck])
&& in_array($inheritancePath, $this->inheritanceStack[$pathToCheck])
) {
$this->inheritanceStack[$pathToCheck][] = $inheritancePath;
throw new CycleInheritancesException(
$pathToCheck . ' has cycle inheritances',
1474900799
);
}
$this->inheritanceStack[$pathToCheck][] = $inheritancePath;
$this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
}
$this->inheritanceStack[$pathToCheck] = null;
}
}
}
/**
* Recursively remove self::INHERITANCE_OPERATOR keys
*
* @param array $array
* @return array the modified array
*/
protected function removeInheritanceOperatorRecursive(array $array): array
{
$result = $array;
foreach ($result as $key => $value) {
if ($key === self::INHERITANCE_OPERATOR) {
unset($result[$key]);
continue;
}
if (is_array($value)) {
$result[$key] = $this->removeInheritanceOperatorRecursive($value);
}
}
return $result;
}
/**
* Check the given array representation of a YAML config for the given path and return it's value / sub-array.
* If path is not found, return null;
*
* @param array $config
* @param string $path
* @param string $delimiter
* @return string|array|null
*/
protected function getValueByPath(array $config, string $path, string $delimiter = '.')
{
try {
return ArrayUtility::getValueByPath($config, $path, $delimiter);
} catch (MissingArrayPathException $exception) {
return null;
}
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Utilities to manage and convert TypoScript
*
* Scope: frontend
*/
class TypoScriptService
{
/**
* @var \TYPO3\CMS\Core\TypoScript\TypoScriptService
*/
protected $extbaseTypoScriptService;
/**
* @param \TYPO3\CMS\Core\TypoScript\TypoScriptService $typoScriptService
* @internal
*/
public function injectTypoScriptService(\TYPO3\CMS\Core\TypoScript\TypoScriptService $typoScriptService)
{
$this->extbaseTypoScriptService = $typoScriptService;
}
/**
* Parse an configuration with ContentObjectRenderer::cObjGetSingle()
* and return the result.
*
* @param array $configuration
* @return array
* @internal
*/
public function resolvePossibleTypoScriptConfiguration(array $configuration = []): array
{
$configuration = $this->extbaseTypoScriptService->convertPlainArrayToTypoScriptArray($configuration);
$configuration = $this->resolveTypoScriptConfiguration($configuration);
$configuration = $this->extbaseTypoScriptService->convertTypoScriptArrayToPlainArray($configuration);
return $configuration;
}
/**
* Parse an configuration with ContentObjectRenderer::cObjGetSingle()
* if there is an array key without and with a dot at the end.
* This sample would be identified as a TypoScript parsable configuration
* part:
*
* [
* 'example' => 'TEXT'
* 'example.' => [
* 'value' => 'some value'
* ]
* ]
*
* @param array $configuration
* @return array
*/
protected function resolveTypoScriptConfiguration(array $configuration = []): array
{
foreach ($configuration as $key => $value) {
$keyWithoutDot = rtrim((string)$key, '.');
if (isset($configuration[$keyWithoutDot]) && isset($configuration[$keyWithoutDot . '.'])) {
$value = $this->getTypoScriptFrontendController()->cObj->cObjGetSingle(
$configuration[$keyWithoutDot],
$configuration[$keyWithoutDot . '.'],
$keyWithoutDot
);
$configuration[$keyWithoutDot] = $value;
} elseif (!isset($configuration[$keyWithoutDot]) && isset($configuration[$keyWithoutDot . '.'])) {
$configuration[$keyWithoutDot] = $this->resolveTypoScriptConfiguration($value);
}
unset($configuration[$keyWithoutDot . '.']);
}
return $configuration;
}
/**
* @return TypoScriptFrontendController
*/
protected function getTypoScriptFrontendController()
{
return $GLOBALS['TSFE'];
}
}
@@ -0,0 +1,196 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Configuration;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
/**
* Configuration source based on YAML files
*
* Scope: frontend / backend
* @internal
*/
class YamlSource
{
/**
* @var FilePersistenceSlot
*/
protected $filePersistenceSlot;
/**
* @param FilePersistenceSlot $filePersistenceSlot
*/
public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
{
$this->filePersistenceSlot = $filePersistenceSlot;
}
/**
* Loads the specified configuration files and returns its merged content
* as an array.
*
* @param array $filesToLoad
* @return array
* @throws ParseErrorException
* @throws NoSuchFileException
* @internal
*/
public function load(array $filesToLoad): array
{
$configuration = [];
foreach ($filesToLoad as $fileToLoad) {
if ($fileToLoad instanceof File) {
$fileIdentifier = $fileToLoad->getIdentifier();
$rawYamlContent = $fileToLoad->getContents();
if ($rawYamlContent === false) {
throw new NoSuchFileException(
'The file "' . $fileIdentifier . '" does not exist.',
1498802253
);
}
} else {
$fileIdentifier = $fileToLoad;
$fileToLoad = GeneralUtility::getFileAbsFileName($fileToLoad);
if (is_file($fileToLoad)) {
$rawYamlContent = file_get_contents($fileToLoad);
} else {
throw new NoSuchFileException(
'The file "' . $fileToLoad . '" does not exist.',
1471473378
);
}
}
try {
$loadedConfiguration = Yaml::parse($rawYamlContent);
if (is_array($loadedConfiguration)) {
$configuration = array_replace_recursive($configuration, $loadedConfiguration);
}
} catch (ParseException $exception) {
throw new ParseErrorException(
'An error occurred while parsing file "' . $fileIdentifier . '": ' . $exception->getMessage(),
1480195405
);
}
}
$configuration = ArrayUtility::convertBooleanStringsToBooleanRecursive($configuration);
return $configuration;
}
/**
* Save the specified configuration array to the given file in YAML format.
*
* @param File|string $fileToSave The file to write to.
* @param array $configuration The configuration to save
* @return mixed
* @throws FileWriteException if the file could not be written
* @internal
*/
public function save($fileToSave, array $configuration)
{
try {
$header = $this->getHeaderFromFile($fileToSave);
} catch (InsufficientFileAccessPermissionsException $e) {
throw new FileWriteException($e->getMessage(), 1512584488, $e);
}
$yaml = Yaml::dump($configuration, 99, 2);
if ($fileToSave instanceof File) {
try {
$this->filePersistenceSlot->allowInvocation(
FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
$this->buildCombinedIdentifier(
$fileToSave->getParentFolder(),
$fileToSave->getName()
),
$this->filePersistenceSlot->getContentSignature(
$header . LF . $yaml
)
);
$fileToSave->setContents($header . LF . $yaml);
} catch (InsufficientFileAccessPermissionsException $e) {
throw new FileWriteException($e->getMessage(), 1512582753, $e);
}
} else {
$byteCount = @file_put_contents($fileToSave, $header . LF . $yaml);
if ($byteCount === false) {
$error = error_get_last();
throw new FileWriteException($error['message'], 1512582929);
}
}
return $return;
}
/**
* Read the header part from the given file. That means, every line
* until the first non comment line is found.
*
* @param File|string $file
* @return string The header of the given YAML file
*/
protected function getHeaderFromFile($file): string
{
$header = '';
if ($file instanceof File) {
$fileLines = explode(LF, $file->getContents());
} elseif (is_file($file)) {
$fileLines = file($file);
} else {
return '';
}
foreach ($fileLines as $line) {
if (preg_match('/^#/', $line)) {
$header .= $line;
} else {
break;
}
}
return $header;
}
/**
* @param FolderInterface $folder
* @param string $fileName
* @return string
*/
protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
{
return sprintf(
'%d:%s%s',
$folder->getStorage()->getUid(),
$folder->getIdentifier(),
$fileName
);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Exception as FormException;
/**
* A generic Form persistence Exception
*
* @internal
*/
class Exception extends FormException
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Persistence\Exception;
/**
* @internal
*/
class NoUniqueIdentifierException extends Exception
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Persistence\Exception;
/**
* @internal
*/
class NoUniquePersistenceIdentifierException extends Exception
{
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Mvc\Persistence\Exception;
/**
* Generic Persistence Manager Exception, to be thrown f.e. if a given form is not loadable
*
* @internal
*/
class PersistenceManagerException extends Exception
{
}
@@ -0,0 +1,951 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniquePersistenceIdentifierException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
/**
* Concrete implementation of the FormPersistenceManagerInterface
*
* Scope: frontend / backend
*/
class FormPersistenceManager implements FormPersistenceManagerInterface
{
const FORM_DEFINITION_FILE_EXTENSION = '.form.yaml';
/**
* @var \TYPO3\CMS\Form\Mvc\Configuration\YamlSource
*/
protected $yamlSource;
/**
* @var \TYPO3\CMS\Core\Resource\StorageRepository
*/
protected $storageRepository;
/**
* @var array
*/
protected $formSettings;
/**
* @var FilePersistenceSlot
*/
protected $filePersistenceSlot;
/**
* @var FrontendInterface
*/
protected $runtimeCache;
/**
* @var ResourceFactory
*/
protected $resourceFactory;
/**
* @param \TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource
* @internal
*/
public function injectYamlSource(\TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource)
{
$this->yamlSource = $yamlSource;
}
/**
* @param \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository
* @internal
*/
public function injectStorageRepository(\TYPO3\CMS\Core\Resource\StorageRepository $storageRepository)
{
$this->storageRepository = $storageRepository;
}
/**
* @param \TYPO3\CMS\Form\Slot\FilePersistenceSlot $filePersistenceSlot
*/
public function injectFilePersistenceSlot(\TYPO3\CMS\Form\Slot\FilePersistenceSlot $filePersistenceSlot)
{
$this->filePersistenceSlot = $filePersistenceSlot;
}
/**
* @param \TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory
*/
public function injectResourceFactory(\TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory)
{
$this->resourceFactory = $resourceFactory;
}
/**
* @internal
*/
public function initializeObject()
{
$this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
->get(ConfigurationManagerInterface::class)
->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
$this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
}
/**
* Load the array formDefinition identified by $persistenceIdentifier, and return it.
* Only files with the extension .yaml or .form.yaml are loaded.
* Form definition file names which not ends with ".form.yaml" has been
* deprecated in TYPO3 v9 and will not be supported in TYPO3 v10.0.
*
* @param string $persistenceIdentifier
* @return array
* @internal
*/
public function load(string $persistenceIdentifier): array
{
$cacheKey = 'formLoad' . md5($persistenceIdentifier);
$yaml = $this->runtimeCache->get($cacheKey);
if ($yaml !== false) {
return $yaml;
}
$file = $this->retrieveFileByPersistenceIdentifier($persistenceIdentifier);
try {
$yaml = $this->yamlSource->load([$file]);
$this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
} catch (\Exception $e) {
$yaml = [
'type' => 'Form',
'identifier' => $persistenceIdentifier,
'label' => $e->getMessage(),
'invalid' => true,
];
}
$this->runtimeCache->set($cacheKey, $yaml);
return $yaml;
}
/**
* Save the array form representation identified by $persistenceIdentifier.
* Only files with the extension .form.yaml are saved.
* If the formDefinition is located within a EXT: resource, save is only
* allowed if the configuration path
* TYPO3.CMS.Form.persistenceManager.allowSaveToExtensionPaths
* is set to true.
*
* @param string $persistenceIdentifier
* @param array $formDefinition
* @throws PersistenceManagerException
* @internal
*/
public function save(string $persistenceIdentifier, array $formDefinition)
{
if (!$this->hasValidFileExtension($persistenceIdentifier)) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1477679820);
}
if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
if (!$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
throw new PersistenceManagerException('Save to extension paths is not allowed.', 1477680881);
}
if (!$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
$message = sprintf('The file "%s" could not be saved. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
throw new PersistenceManagerException($message, 1484073571);
}
$fileToSave = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
} else {
$fileToSave = $this->getOrCreateFile($persistenceIdentifier);
}
try {
$this->yamlSource->save($fileToSave, $formDefinition);
} catch (FileWriteException $e) {
throw new PersistenceManagerException(sprintf(
'The file "%s" could not be saved: %s',
$persistenceIdentifier,
$e->getMessage()
), 1512582637, $e);
}
}
/**
* Delete the form representation identified by $persistenceIdentifier.
* Only files with the extension .form.yaml are removed.
*
* @param string $persistenceIdentifier
* @throws PersistenceManagerException
* @internal
*/
public function delete(string $persistenceIdentifier)
{
if (!$this->hasValidFileExtension($persistenceIdentifier)) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239534);
}
if (!$this->exists($persistenceIdentifier)) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239535);
}
if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
if (!$this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths']) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239536);
}
if (!$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
$message = sprintf('The file "%s" could not be removed. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
throw new PersistenceManagerException($message, 1484073878);
}
$fileToDelete = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
unlink($fileToDelete);
} else {
list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
$storage = $this->getStorageByUid((int)$storageUid);
$file = $storage->getFile($fileIdentifier);
if (!$storage->checkFileActionPermission('delete', $file)) {
throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $persistenceIdentifier), 1472239516);
}
$storage->deleteFile($file);
}
}
/**
* Check whether a form with the specified $persistenceIdentifier exists
*
* @param string $persistenceIdentifier
* @return bool TRUE if a form with the given $persistenceIdentifier can be loaded, otherwise FALSE
* @internal
*/
public function exists(string $persistenceIdentifier): bool
{
$exists = false;
if ($this->hasValidFileExtension($persistenceIdentifier)) {
if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
if ($this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
$exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
}
} else {
list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
$storage = $this->getStorageByUid((int)$storageUid);
$exists = $storage->hasFile($fileIdentifier);
}
}
return $exists;
}
/**
* List all form definitions which can be loaded through this form persistence
* manager.
*
* Returns an associative array with each item containing the keys 'name' (the human-readable name of the form)
* and 'persistenceIdentifier' (the unique identifier for the Form Persistence Manager e.g. the path to the saved form definition).
*
* @return array in the format [['name' => 'Form 01', 'persistenceIdentifier' => 'path1'], [ .... ]]
* @internal
*/
public function listForms(): array
{
$identifiers = [];
$forms = [];
foreach ($this->retrieveYamlFilesFromStorageFolders() as $file) {
/** @var Folder $folder */
$folder = $file->getParentFolder();
// TODO: deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
$formReadOnly = $folder->getCombinedIdentifier() === '1:/user_upload/';
$form = $this->loadMetaData($file);
if (!$this->looksLikeAFormDefinition($form)) {
continue;
}
$persistenceIdentifier = $file->getCombinedIdentifier();
if ($this->hasValidFileExtension($persistenceIdentifier)) {
$forms[] = [
'identifier' => $form['identifier'],
'name' => $form['label'] ?? $form['identifier'],
'persistenceIdentifier' => $persistenceIdentifier,
'readOnly' => $formReadOnly,
'removable' => true,
'location' => 'storage',
'duplicateIdentifier' => false,
'invalid' => $form['invalid'],
'fileUid' => $form['fileUid'],
];
$identifiers[$form['identifier']]++;
} else {
$forms[] = [
'identifier' => $form['identifier'],
'name' => $form['label'] ?? $form['identifier'],
'persistenceIdentifier' => $persistenceIdentifier,
'readOnly' => true,
'removable' => false,
'location' => 'storage',
'duplicateIdentifier' => false,
'invalid' => false,
'deprecatedFileExtension' => true,
'fileUid' => $form['fileUid'],
];
}
}
foreach ($this->retrieveYamlFilesFromExtensionFolders() as $fullPath => $fileName) {
$form = $this->loadMetaData($fullPath);
if ($this->looksLikeAFormDefinition($form)) {
if ($this->hasValidFileExtension($fileName)) {
$forms[] = [
'identifier' => $form['identifier'],
'name' => $form['label'] ?? $form['identifier'],
'persistenceIdentifier' => $fullPath,
'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
'location' => 'extension',
'duplicateIdentifier' => false,
'invalid' => $form['invalid'],
'fileUid' => $form['fileUid'],
];
$identifiers[$form['identifier']]++;
} else {
$forms[] = [
'identifier' => $form['identifier'],
'name' => $form['label'] ?? $form['identifier'],
'persistenceIdentifier' => $fullPath,
'readOnly' => true,
'removable' => false,
'location' => 'extension',
'duplicateIdentifier' => false,
'invalid' => false,
'deprecatedFileExtension' => true,
'fileUid' => $form['fileUid'],
];
}
}
}
foreach ($identifiers as $identifier => $count) {
if ($count > 1) {
foreach ($forms as &$formDefinition) {
if ($formDefinition['identifier'] === $identifier) {
$formDefinition['duplicateIdentifier'] = true;
}
}
}
}
return $forms;
}
/**
* Retrieves yaml files from storage folders for further processing.
* At this time it's not determined yet, whether these files contain form data.
*
* @return File[]
* @internal
*/
public function retrieveYamlFilesFromStorageFolders(): array
{
$filesFromStorageFolders = [];
$fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
$fileExtensionFilter->setAllowedFileExtensions(['yaml']);
foreach ($this->getAccessibleFormStorageFolders() as $folder) {
$storage = $folder->getStorage();
$storage->setFileAndFolderNameFilters([
[$fileExtensionFilter, 'filterFileList']
]);
$files = $folder->getFiles(
0,
0,
Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS,
true
);
$filesFromStorageFolders = array_merge($filesFromStorageFolders, array_values($files));
$storage->resetFileAndFolderNameFiltersToDefault();
}
return $filesFromStorageFolders;
}
/**
* Retrieves yaml files from extension folders for further processing.
* At this time it's not determined yet, whether these files contain form data.
*
* @return File[]
* @internal
*/
public function retrieveYamlFilesFromExtensionFolders(): array
{
$filesFromExtensionFolders = [];
foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
if ($fileInfo->getExtension() !== 'yaml') {
continue;
}
$filesFromExtensionFolders[$relativePath . $fileInfo->getFilename()] = $fileInfo->getFilename();
}
}
return $filesFromExtensionFolders;
}
/**
* Return a list of all accessible file mountpoints for the
* current backend user.
*
* Only registered mountpoints from
* TYPO3.CMS.Form.persistenceManager.allowedFileMounts
* are listet.
*
* @return Folder[]
* @internal
*/
public function getAccessibleFormStorageFolders(): array
{
$storageFolders = [];
if (
!isset($this->formSettings['persistenceManager']['allowedFileMounts'])
|| !is_array($this->formSettings['persistenceManager']['allowedFileMounts'])
|| empty($this->formSettings['persistenceManager']['allowedFileMounts'])
) {
return $storageFolders;
}
foreach ($this->formSettings['persistenceManager']['allowedFileMounts'] as $allowedFileMount) {
$allowedFileMount = rtrim($allowedFileMount, '/') . '/';
// $fileMountPath is like "/form_definitions/" or "/group_homes/1/form_definitions/"
[$storageUid, $fileMountPath] = explode(':', $allowedFileMount, 2);
try {
$storage = $this->getStorageByUid((int)$storageUid);
} catch (PersistenceManagerException $e) {
continue;
}
$isStorageFileMount = false;
$parentFolder = $storage->getRootLevelFolder(false);
foreach ($storage->getFileMounts() as $storageFileMount) {
/** @var \TYPO3\CMS\Core\Resource\Folder */
$storageFileMountFolder = $storageFileMount['folder'];
// Normally should use ResourceStorage::isWithinFolder() to check if the configured file mount path is within a storage file mount but this requires a valid Folder object and thus a directory which already exists. And the folder could simply not exist yet.
if (StringUtility::beginsWith($fileMountPath, $storageFileMountFolder->getIdentifier())) {
$isStorageFileMount = true;
$parentFolder = $storageFileMountFolder;
}
}
// Get storage folder object, create it if missing
try {
$fileMountFolder = $storage->getFolder($fileMountPath);
} catch (InsufficientFolderAccessPermissionsException $e) {
continue;
} catch (FolderDoesNotExistException $e) {
if ($isStorageFileMount) {
$fileMountPath = substr(
$fileMountPath,
strlen($parentFolder->getIdentifier())
);
}
try {
$fileMountFolder = $storage->createFolder($fileMountPath, $parentFolder);
} catch (InsufficientFolderAccessPermissionsException $e) {
continue;
}
}
$storageFolders[$allowedFileMount] = $fileMountFolder;
}
return $storageFolders;
}
/**
* Return a list of all accessible extension folders
*
* Only registered mountpoints from
* TYPO3.CMS.Form.persistenceManager.allowedExtensionPaths
* are listet.
*
* @return array
* @internal
*/
public function getAccessibleExtensionFolders(): array
{
$extensionFolders = $this->runtimeCache->get('formAccessibleExtensionFolders');
if ($extensionFolders !== false) {
return $extensionFolders;
}
$extensionFolders = [];
if (
!isset($this->formSettings['persistenceManager']['allowedExtensionPaths'])
|| !is_array($this->formSettings['persistenceManager']['allowedExtensionPaths'])
|| empty($this->formSettings['persistenceManager']['allowedExtensionPaths'])
) {
$this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
return $extensionFolders;
}
foreach ($this->formSettings['persistenceManager']['allowedExtensionPaths'] as $allowedExtensionPath) {
if (!$this->pathIsIntendedAsExtensionPath($allowedExtensionPath)) {
continue;
}
$allowedExtensionFullPath = GeneralUtility::getFileAbsFileName($allowedExtensionPath);
if (!file_exists($allowedExtensionFullPath)) {
continue;
}
$allowedExtensionPath = rtrim($allowedExtensionPath, '/') . '/';
$extensionFolders[$allowedExtensionPath] = $allowedExtensionFullPath;
}
$this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
return $extensionFolders;
}
/**
* This takes a form identifier and returns a unique persistence identifier for it.
* By default this is just similar to the identifier. But if a form with the same persistence identifier already
* exists a suffix is appended until the persistence identifier is unique.
*
* @param string $formIdentifier lowerCamelCased form identifier
* @param string $savePath
* @return string unique form persistence identifier
* @throws NoUniquePersistenceIdentifierException
* @internal
*/
public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string
{
$savePath = rtrim($savePath, '/') . '/';
$formPersistenceIdentifier = $savePath . $formIdentifier . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
for ($attempts = 1; $attempts < 100; $attempts++) {
$formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
}
$formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
throw new NoUniquePersistenceIdentifierException(
sprintf('Could not find a unique persistence identifier for form identifier "%s" after %d attempts', $formIdentifier, $attempts),
1476010403
);
}
/**
* This takes a form identifier and returns a unique identifier for it.
* If a formDefinition with the same identifier already exists a suffix is
* appended until the identifier is unique.
*
* @param string $identifier
* @return string unique form identifier
* @throws NoUniqueIdentifierException
* @internal
*/
public function getUniqueIdentifier(string $identifier): string
{
$originalIdentifier = $identifier;
if ($this->checkForDuplicateIdentifier($identifier)) {
for ($attempts = 1; $attempts < 100; $attempts++) {
$identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
if (!$this->checkForDuplicateIdentifier($identifier)) {
return $identifier;
}
}
$identifier = $originalIdentifier . '_' . time();
if ($this->checkForDuplicateIdentifier($identifier)) {
throw new NoUniqueIdentifierException(
sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
1477688567
);
}
}
return $identifier;
}
/**
* Check if a identifier is already used by a formDefintion.
*
* @param string $identifier
* @return bool
* @internal
*/
public function checkForDuplicateIdentifier(string $identifier): bool
{
$identifierUsed = false;
foreach ($this->listForms() as $formDefinition) {
if ($formDefinition['identifier'] === $identifier) {
$identifierUsed = true;
break;
}
}
return $identifierUsed;
}
/**
* Check if a persistence path or if a persistence identifier path
* is configured within the form setup
* (TYPO3.CMS.Form.persistenceManager.allowedExtensionPaths / TYPO3.CMS.Form.persistenceManager.allowedFileMounts).
* If the input is a persistence identifier an additional check for a
* valid file extension will be performed.
* .
* @param string $persistencePath
* @return bool
* @internal
*/
public function isAllowedPersistencePath(string $persistencePath): bool
{
$pathinfo = PathUtility::pathinfo($persistencePath);
$persistencePathIsFile = isset($pathinfo['extension']);
if (
$persistencePathIsFile
&& $this->pathIsIntendedAsExtensionPath($persistencePath)
&& $this->hasValidFileExtension($persistencePath)
&& $this->isFileWithinAccessibleExtensionFolders($persistencePath)
) {
return true;
}
if (
$persistencePathIsFile
&& $this->pathIsIntendedAsFileMountPath($persistencePath)
&& $this->hasValidFileExtension($persistencePath)
&& $this->isFileWithinAccessibleFormStorageFolders($persistencePath)
) {
return true;
}
if (
!$persistencePathIsFile
&& $this->pathIsIntendedAsExtensionPath($persistencePath)
&& $this->isAccessibleExtensionFolder($persistencePath)
) {
return true;
}
if (
!$persistencePathIsFile
&& $this->pathIsIntendedAsFileMountPath($persistencePath)
&& $this->isAccessibleFormStorageFolder($persistencePath)
) {
return true;
}
return false;
}
/**
* @param string $path
* @return bool
*/
protected function pathIsIntendedAsExtensionPath(string $path): bool
{
return strpos($path, 'EXT:') === 0;
}
/**
* @param string $path
* @return bool
*/
protected function pathIsIntendedAsFileMountPath(string $path): bool
{
if (empty($path)) {
return false;
}
[$storageUid, $pathIdentifier] = explode(':', $path, 2);
if (empty($storageUid) || empty($pathIdentifier)) {
return false;
}
return MathUtility::canBeInterpretedAsInteger($storageUid);
}
/**
* Returns a File object for a given $persistenceIdentifier.
* If no file for this identifier exists a new object will be
* created.
*
* @param string $persistenceIdentifier
* @return File
* @throws PersistenceManagerException
*/
protected function getOrCreateFile(string $persistenceIdentifier): File
{
list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
$storage = $this->getStorageByUid((int)$storageUid);
$pathinfo = PathUtility::pathinfo($fileIdentifier);
if (!$storage->hasFolder($pathinfo['dirname'])) {
throw new PersistenceManagerException(sprintf('Could not create folder "%s".', $pathinfo['dirname']), 1471630579);
}
try {
$folder = $storage->getFolder($pathinfo['dirname']);
} catch (InsufficientFolderAccessPermissionsException $e) {
throw new PersistenceManagerException(sprintf('No read access to folder "%s".', $pathinfo['dirname']), 1512583307);
}
if (!$storage->checkFolderActionPermission('write', $folder)) {
throw new PersistenceManagerException(sprintf('No write access to folder "%s".', $pathinfo['dirname']), 1471630580);
}
if (!$storage->hasFile($fileIdentifier)) {
$this->filePersistenceSlot->allowInvocation(
FilePersistenceSlot::COMMAND_FILE_CREATE,
$folder->getCombinedIdentifier() . $pathinfo['basename']
);
$file = $folder->createFile($pathinfo['basename']);
} else {
$file = $storage->getFile($fileIdentifier);
}
return $file;
}
/**
* Returns a ResourceStorage for a given uid
*
* @param int $storageUid
* @return ResourceStorage
* @throws PersistenceManagerException
*/
protected function getStorageByUid(int $storageUid): ResourceStorage
{
$storage = $this->storageRepository->findByUid($storageUid);
if (
!$storage instanceof ResourceStorage
|| !$storage->isBrowsable()
) {
throw new PersistenceManagerException(sprintf('Could not access storage with uid "%d".', $storageUid), 1471630581);
}
return $storage;
}
/**
* @param string|File $persistenceIdentifier
* @return array
* @throws NoSuchFileException
*/
protected function loadMetaData($persistenceIdentifier): array
{
if ($persistenceIdentifier instanceof File) {
$file = $persistenceIdentifier;
$persistenceIdentifier = $file->getCombinedIdentifier();
} else {
$file = $this->retrieveFileByPersistenceIdentifier($persistenceIdentifier);
}
try {
$rawYamlContent = $file->getContents();
if ($rawYamlContent === false) {
throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684462);
}
$yaml = $this->extractMetaDataFromCouldBeFormDefinition($rawYamlContent);
$this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
$yaml['fileUid'] = $file->getUid();
} catch (\Exception $e) {
$yaml = [
'type' => 'Form',
'identifier' => $persistenceIdentifier,
'label' => $e->getMessage(),
'invalid' => true,
];
}
return $yaml;
}
/**
* @param string $maybeRawFormDefinition
* @return array
*/
protected function extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition): array
{
$metaDataProperties = ['identifier', 'type', 'label', 'prototypeName'];
$metaData = [];
foreach (explode(LF, $maybeRawFormDefinition) as $line) {
if (empty($line) || $line[0] === ' ') {
continue;
}
[$key, $value] = explode(':', $line);
if (
empty($key)
|| empty($value)
|| !in_array($key, $metaDataProperties, true)
) {
continue;
}
$value = trim($value, " '\"\r");
$metaData[$key] = $value;
}
return $metaData;
}
/**
* @param array $formDefinition
* @param string $persistenceIdentifier
* @throws PersistenceManagerException
*/
protected function generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension(array $formDefinition, string $persistenceIdentifier): void
{
if (
$this->looksLikeAFormDefinition($formDefinition)
&& !$this->hasValidFileExtension($persistenceIdentifier)
) {
if (strpos($persistenceIdentifier, 'EXT:') === 0) {
trigger_error(
'Form definition file name ("' . $persistenceIdentifier . '") which does not end with ".form.yaml" will not be supported in TYPO3 v10.0.',
E_USER_DEPRECATED
);
} elseif (strpos($persistenceIdentifier, 'EXT:') !== 0) {
throw new PersistenceManagerException(sprintf('Form definition "%s" does not end with ".form.yaml".', $persistenceIdentifier), 1531160649);
}
}
}
/**
* @param string $persistenceIdentifier
* @return File
* @throws PersistenceManagerException
* @throws NoSuchFileException
*/
protected function retrieveFileByPersistenceIdentifier(string $persistenceIdentifier): File
{
if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1477679819);
}
if (
$this->pathIsIntendedAsExtensionPath($persistenceIdentifier)
&& !$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)
) {
$message = sprintf('The file "%s" could not be loaded. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
throw new PersistenceManagerException($message, 1484071985);
}
try {
$file = $this->resourceFactory->retrieveFileOrFolderObject($persistenceIdentifier);
} catch (\Exception $e) {
// Top level catch to ensure useful following exception handling, because FAL throws top level exceptions.
$file = null;
}
if ($file === null) {
throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684442);
}
if (!$file->getStorage()->checkFileActionPermission('read', $file)) {
throw new PersistenceManagerException(sprintf('No read access to file "%s".', $persistenceIdentifier), 1471630578);
}
return $file;
}
/**
* @param string $fileName
* @return bool
*/
protected function hasValidFileExtension(string $fileName): bool
{
return StringUtility::endsWith($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
}
/**
* @param string $fileName
* @return bool
*/
protected function isFileWithinAccessibleExtensionFolders(string $fileName): bool
{
$dirName = rtrim(PathUtility::pathinfo($fileName, PATHINFO_DIRNAME), '/') . '/';
return array_key_exists($dirName, $this->getAccessibleExtensionFolders());
}
/**
* @param string $fileName
* @return bool
*/
protected function isFileWithinAccessibleFormStorageFolders(string $fileName): bool
{
$pathInfo = PathUtility::pathinfo($fileName, PATHINFO_DIRNAME);
$pathInfo = is_string($pathInfo) ? $pathInfo : '';
$dirName = rtrim($pathInfo, '/') . '/';
return array_key_exists($dirName, $this->getAccessibleFormStorageFolders());
}
/**
* @param string $folderName
* @return bool
*/
protected function isAccessibleExtensionFolder(string $folderName): bool
{
$folderName = rtrim($folderName, '/') . '/';
return array_key_exists($folderName, $this->getAccessibleExtensionFolders());
}
/**
* @param string $folderName
* @return bool
*/
protected function isAccessibleFormStorageFolder(string $folderName): bool
{
$folderName = rtrim($folderName, '/') . '/';
return array_key_exists($folderName, $this->getAccessibleFormStorageFolders());
}
/**
* @param array $data
* @return bool
*/
protected function looksLikeAFormDefinition(array $data): bool
{
return isset($data['identifier'], $data['type']) && !empty($data['identifier']) && $data['type'] === 'Form';
}
}
@@ -0,0 +1,102 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Persistence;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\Folder;
/**
* The form persistence manager interface
*
* Scope: frontend / backend
*/
interface FormPersistenceManagerInterface
{
/**
* Load the array form representation identified by $persistenceIdentifier, and return it
*
* @param string $persistenceIdentifier
* @return array
*/
public function load(string $persistenceIdentifier): array;
/**
* Save the array form representation identified by $persistenceIdentifier
*
* @param string $persistenceIdentifier
* @param array $formDefinition
*/
public function save(string $persistenceIdentifier, array $formDefinition);
/**
* Check whether a form with the specified $persistenceIdentifier exists
*
* @param string $persistenceIdentifier
* @return bool TRUE if a form with the given $persistenceIdentifier can be loaded, otherwise FALSE
*/
public function exists(string $persistenceIdentifier): bool;
/**
* Delete the form representation identified by $persistenceIdentifier
*
* @param string $persistenceIdentifier
*/
public function delete(string $persistenceIdentifier);
/**
* List all form definitions which can be loaded through this form persistence
* manager.
*
* Returns an associative array with each item containing the keys 'name' (the human-readable name of the form)
* and 'persistenceIdentifier' (the unique identifier for the Form Persistence Manager e.g. the path to the saved form definition).
*
* @return array in the format [['name' => 'Form 01', 'persistenceIdentifier' => 'path1'], [ .... ]]
*/
public function listForms(): array;
/**
* Return a list of all accessible file mount points
*
* @return Folder[]
*/
public function getAccessibleFormStorageFolders(): array;
/**
* Return a list of all accessible extension folders
*
* @return array
*/
public function getAccessibleExtensionFolders(): array;
/**
* This takes a form identifier and returns a unique persistence identifier for it.
*
* @param string $formIdentifier
* @param string $savePath
* @return string
*/
public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string;
/**
* Check if a identifier is already used by a formDefintion.
*
* @param string $identifier
* @return bool
*/
public function checkForDuplicateIdentifier(string $identifier): bool;
}
@@ -0,0 +1,187 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration;
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
/**
* A processing Rule contains information for property mapping and validation.
*
* Scope: frontend
* **This class is NOT meant to be sub classed by developers.**
* @internal
*/
class ProcessingRule
{
/**
* The target data type the data should be converted to
*
* @var string
*/
protected $dataType;
/**
* @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration
*/
protected $propertyMappingConfiguration;
/**
* @var \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator
*/
protected $validator;
/**
* @var \TYPO3\CMS\Extbase\Error\Result
*/
protected $processingMessages;
/**
* @var \TYPO3\CMS\Extbase\Property\PropertyMapper
*/
protected $propertyMapper;
/**
* @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration
* @internal
*/
public function injectPropertyMappingConfiguration(\TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration)
{
$this->propertyMappingConfiguration = $propertyMappingConfiguration;
}
/**
* @param \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator $validator
* @internal
*/
public function injectConjunctionValidator(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator $validator)
{
$this->validator = $validator;
}
/**
* @param \TYPO3\CMS\Extbase\Property\PropertyMapper $propertyMapper
* @internal
*/
public function injectPropertyMapper(\TYPO3\CMS\Extbase\Property\PropertyMapper $propertyMapper)
{
$this->propertyMapper = $propertyMapper;
}
/**
* Constructs this processing rule
* @internal
*/
public function __construct()
{
$this->processingMessages = GeneralUtility::makeInstance(ObjectManager::class)
->get(Result::class);
}
/**
* @return PropertyMappingConfiguration
* @internal
*/
public function getPropertyMappingConfiguration(): PropertyMappingConfiguration
{
return $this->propertyMappingConfiguration;
}
/**
* @return string
* @internal
*/
public function getDataType(): string
{
return $this->dataType;
}
/**
* @param string $dataType
* @internal
*/
public function setDataType(string $dataType)
{
$this->dataType = $dataType;
}
/**
* Returns the child validators of the ConjunctionValidator that is bound to this processing rule
*
* @return \SplObjectStorage<ValidatorInterface>
* @internal
*/
public function getValidators(): \SplObjectStorage
{
return $this->validator->getValidators();
}
/**
* @param ValidatorInterface $validator
* @internal
*/
public function addValidator(ValidatorInterface $validator)
{
$this->validator->addValidator($validator);
}
/**
* Removes the specified validator.
*
* @param ValidatorInterface $validator The validator to remove
* @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
* @internal
*/
public function removeValidator(ValidatorInterface $validator)
{
$this->validator->removeValidator($validator);
}
/**
* @param mixed $value
* @return mixed
* @internal
*/
public function process($value)
{
if ($this->dataType !== null) {
$value = $this->propertyMapper->convert($value, $this->dataType, $this->propertyMappingConfiguration);
$messages = $this->propertyMapper->getMessages();
} else {
$messages = GeneralUtility::makeInstance(ObjectManager::class)
->get(Result::class);
}
$validationResult = $this->validator->validate($value);
$messages->merge($validationResult);
$this->processingMessages->merge($messages);
return $value;
}
/**
* @return Result
* @internal
*/
public function getProcessingMessages(): Result
{
return $this->processingMessages;
}
}
@@ -0,0 +1,45 @@
<?php
namespace TYPO3\CMS\Form\Mvc\Property\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Error\Error;
/**
* A "Type Converter" Exception
*/
class TypeConverterException extends \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException
{
/**
* @var Error
*/
protected $error;
public static function fromError(Error $error): TypeConverterException
{
$exception = new static($error->getMessage(), $error->getCode());
$exception->error = $error;
return $exception;
}
public function getError(): Error
{
if (empty($this->error)) {
return new Error($this->getMessage(), $this->getCode(), [$this->getPrevious()]);
}
return $this->error;
}
}
@@ -0,0 +1,187 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Property;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;
use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;
use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle\AfterFormStateInitializedInterface;
use TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter;
use TYPO3\CMS\Form\Mvc\Validation\MimeTypeValidator;
/**
* Scope: frontend
* @internal
*/
class PropertyMappingConfiguration implements AfterFormStateInitializedInterface
{
/**
* This hook is called for each form element after the class
* TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory has built the entire form.
*
* It is invoked after the static form definition is ready, but without knowing
* about the individual state organized in `FormRuntime` and `FormState`.
*
* @param RenderableInterface $renderable
* @internal
*/
public function afterBuildingFinished(RenderableInterface $renderable)
{
if ($renderable instanceof FileUpload) {
// Set the property mapping configuration for the file upload element.
// * Add the UploadedFileReferenceConverter to convert an uploaded file to a
// FileReference.
// * Add the MimeTypeValidator to the UploadedFileReferenceConverter to
// delete non-valid file types directly.
// * Setup the storage:
// If the property "saveToFileMount" exist for this element it will be used.
// If this file mount or the property "saveToFileMount" does not exist
// the default storage "1:/user_uploads/" will be used. Uploads are placed
// in a dedicated sub-folder (e.g. ".../form_<40-chars-hash>/actual.file").
/** @var UploadedFileReferenceConverter $typeConverter */
$typeConverter = GeneralUtility::makeInstance(ObjectManager::class)
->get(UploadedFileReferenceConverter::class);
/** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
$propertyMappingConfiguration = $renderable->getRootForm()
->getProcessingRule($renderable->getIdentifier())
->getPropertyMappingConfiguration()
->setTypeConverter($typeConverter);
$allowedMimeTypes = [];
$validators = [];
if (isset($renderable->getProperties()['allowedMimeTypes']) && \is_array($renderable->getProperties()['allowedMimeTypes'])) {
$allowedMimeTypes = array_filter($renderable->getProperties()['allowedMimeTypes']);
}
if (!empty($allowedMimeTypes)) {
$mimeTypeValidator = GeneralUtility::makeInstance(ObjectManager::class)
->get(MimeTypeValidator::class, ['allowedMimeTypes' => $allowedMimeTypes]);
$validators = [$mimeTypeValidator];
}
$processingRule = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier());
foreach ($processingRule->getValidators() as $validator) {
if (!($validator instanceof NotEmptyValidator)) {
$validators[] = $validator;
$processingRule->removeValidator($validator);
}
}
$uploadConfiguration = [
UploadedFileReferenceConverter::CONFIGURATION_FILE_VALIDATORS => $validators,
UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_CONFLICT_MODE => 'rename',
];
$saveToFileMountIdentifier = $renderable->getProperties()['saveToFileMount'] ?? '';
if ($this->checkSaveFileMountAccess($saveToFileMountIdentifier)) {
$uploadConfiguration[UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_FOLDER] = $saveToFileMountIdentifier;
} else {
// @todo Why should uploaded files be stored to the same directory as the *.form.yaml definitions?
$persistenceIdentifier = $renderable->getRootForm()->getPersistenceIdentifier();
if (!empty($persistenceIdentifier)) {
$pathinfo = PathUtility::pathinfo($persistenceIdentifier);
$saveToFileMountIdentifier = $pathinfo['dirname'];
if ($this->checkSaveFileMountAccess($saveToFileMountIdentifier)) {
$uploadConfiguration[UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_FOLDER] = $saveToFileMountIdentifier;
}
}
}
$propertyMappingConfiguration->setTypeConverterOptions(UploadedFileReferenceConverter::class, $uploadConfiguration);
return;
}
if ($renderable->getType() === 'Date') {
// Set the property mapping configuration for the `Date` element.
/** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */
$propertyMappingConfiguration = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier())->getPropertyMappingConfiguration();
// @see https://www.w3.org/TR/2011/WD-html-markup-20110405/input.date.html#input.date.attrs.value
// 'Y-m-d' = https://tools.ietf.org/html/rfc3339#section-5.6 -> full-date
$propertyMappingConfiguration->setTypeConverterOption(DateTimeConverter::class, DateTimeConverter::CONFIGURATION_DATE_FORMAT, 'Y-m-d');
}
}
/**
* @param string $saveToFileMountIdentifier
* @return bool
* @internal
*/
protected function checkSaveFileMountAccess(string $saveToFileMountIdentifier): bool
{
if (empty($saveToFileMountIdentifier)) {
return false;
}
$resourceFactory = GeneralUtility::makeInstance(ObjectManager::class)
->get(ResourceFactory::class);
try {
$resourceFactory->getFolderObjectFromCombinedIdentifier($saveToFileMountIdentifier);
return true;
} catch (\InvalidArgumentException $e) {
return false;
}
}
/**
* @param FormRuntime $formRuntime holding current form state and static form definition
*/
public function afterFormStateInitialized(FormRuntime $formRuntime): void
{
foreach ($formRuntime->getFormDefinition()->getRenderablesRecursively() as $renderable) {
$this->adjustPropertyMappingForFileUploadsAtRuntime($formRuntime, $renderable);
}
}
/**
* If the form runtime is able to process form submissions
* (determined by $formRuntime->canProcessFormSubmission()) then a
* 'form session' is available.
* This form session identifier will be used to deriving storage sub-folders
* for the file uploads.
* This is done by setting `UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_SEED`
* type converter option.
*
* @param FormRuntime $formRuntime
* @param RenderableInterface $renderable
*/
protected function adjustPropertyMappingForFileUploadsAtRuntime(
FormRuntime $formRuntime,
RenderableInterface $renderable
): void {
if (!$renderable instanceof FileUpload
|| $formRuntime->getFormSession() === null
|| !$formRuntime->canProcessFormSubmission()
) {
return;
}
$renderable->getRootForm()
->getProcessingRule($renderable->getIdentifier())
->getPropertyMappingConfiguration()
->setTypeConverterOption(
UploadedFileReferenceConverter::class,
UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_SEED,
$formRuntime->getFormSession()->getIdentifier()
);
}
}
@@ -0,0 +1,208 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
use TYPO3\CMS\Form\Type\FormDefinitionArray;
/**
* Converter for form definition arrays
*
* @internal
*/
class FormDefinitionArrayConverter extends AbstractTypeConverter
{
/**
* @var array<string>
*/
protected $sourceTypes = ['string'];
/**
* @var string
*/
protected $targetType = FormDefinitionArray::class;
/**
* @var int
*/
protected $priority = 10;
/**
* @var ConfigurationService
*/
protected $configurationService;
/**
* Convert from $source to $targetType, a noop if the source is an array.
* If it is an empty string it will be converted to an empty array.
*
* @param string $source
* @param string $targetType
* @param array $convertedChildProperties
* @param PropertyMappingConfigurationInterface $configuration
* @return FormDefinitionArray
* @throws PropertyException
*/
public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
{
$rawFormDefinitionArray = json_decode($source, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new PropertyException('Unable to decode JSON source: ' . json_last_error_msg(), 1512578002);
}
$formDefinitionValidationService = $this->getFormDefinitionValidationService();
$formDefinitionConversionService = $this->getFormDefinitionConversionService();
// Extend the hmac hashing key with the "per form editor session (load / save)" unique key.
// @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService::addHmacData
$sessionToken = $this->retrieveSessionToken();
$prototypeName = $rawFormDefinitionArray['prototypeName'] ?? null;
$identifier = $rawFormDefinitionArray['identifier'] ?? null;
// A modification of the properties "prototypeName" and "identifier" from the root form element
// through the form editor is always forbidden.
try {
if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'identifier'], $identifier, $rawFormDefinitionArray['_orig_identifier'] ?? [], $sessionToken)) {
throw new PropertyException('Unauthorized modification of "identifier".', 1528538324);
}
if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'prototypeName'], $prototypeName, $rawFormDefinitionArray['_orig_prototypeName'] ?? [], $sessionToken)) {
throw new PropertyException('Unauthorized modification of "prototype name".', 1528538323);
}
} catch (PropertyException $e) {
throw new PropertyException('Unauthorized modification of "prototype name" or "identifier".', 1528538322);
}
$formDefinitionValidationService->validateFormDefinitionProperties($rawFormDefinitionArray, $prototypeName, $sessionToken);
// @todo move all the transformations to FormDefinitionConversionService
$rawFormDefinitionArray = $this->filterEmptyArrays($rawFormDefinitionArray);
$rawFormDefinitionArray = $this->transformMultiValueElementsForFormFramework($rawFormDefinitionArray);
// @todo: replace with rte parsing
$rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
$rawFormDefinitionArray = $formDefinitionConversionService->removeHmacData($rawFormDefinitionArray);
$formDefinitionArray = GeneralUtility::makeInstance(FormDefinitionArray::class, $rawFormDefinitionArray);
return $formDefinitionArray;
}
/**
* Some data which is build by the form editor needs a transformation before
* it can be used by the framework.
* Multivalue elements like select elements produce data like:
*
* [
* _label => 'label'
* _value => 'value'
* ]
*
* This method transforms this into:
*
* [
* 'value' => 'label'
* ]
*
* @param array $input
* @return array
*/
protected function transformMultiValueElementsForFormFramework(array $input): array
{
$output = [];
foreach ($input as $key => $value) {
if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
$key = $value['_value'];
$value = $value['_label'];
}
if (is_array($value)) {
$output[$key] = $this->transformMultiValueElementsForFormFramework($value);
} else {
$output[$key] = $value;
}
}
return $output;
}
/**
* Remove keys from an array if the key value is an empty array
*
* @todo ArrayUtility?
* @param array $array
* @return array
*/
protected function filterEmptyArrays(array $array): array
{
foreach ($array as $key => $value) {
if (!is_array($value)) {
continue;
}
if (empty($value)) {
unset($array[$key]);
continue;
}
$array[$key] = $this->filterEmptyArrays($value);
if (empty($array[$key])) {
unset($array[$key]);
}
}
return $array;
}
/**
* @return string
*/
protected function retrieveSessionToken(): string
{
return $this->getBackendUser()->getSessionData('extFormProtectionSessionToken');
}
/**
* @return FormDefinitionValidationService
*/
protected function getFormDefinitionValidationService(): FormDefinitionValidationService
{
return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
}
/**
* @return FormDefinitionConversionService
*/
protected function getFormDefinitionConversionService(): FormDefinitionConversionService
{
return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Type\File\FileInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Mvc\Property\Exception\TypeConverterException;
/**
* Use in `UploadedFileReferenceConverter` handling file uploads.
* `PseudoFile` and `PseudoFileReference` are independent and not associated.
* @internal
*/
class PseudoFile
{
/**
* @var \SplFileInfo
*/
protected $nameFileInfo;
/**
* @var FileInfo
*/
protected $payloadFileInfo;
/**
* @var string
*/
protected $payloadFilePath;
/**
* see https://www.php.net/manual/en/features.file-upload.post-method.php
*
* @param array $uploadInfo as in $_FILES
* @throws TypeConverterException
*/
public function __construct(array $uploadInfo)
{
if (!isset($uploadInfo['tmp_name']) || !isset($uploadInfo['name'])) {
throw new TypeConverterException(
'Could not determine uploaded file',
1602103603
);
}
$this->nameFileInfo = new \SplFileInfo($uploadInfo['name']);
$this->payloadFilePath = $uploadInfo['tmp_name'];
$this->payloadFileInfo = GeneralUtility::makeInstance(FileInfo::class, $uploadInfo['tmp_name']);
}
public function getName(): string
{
return $this->nameFileInfo->getBasename();
}
public function getNameWithoutExtension(): string
{
// `image...png`
return rtrim(
$this->nameFileInfo->getBasename($this->getExtension()),
'.'
);
}
public function getExtension(): string
{
return $this->nameFileInfo->getExtension();
}
public function getSize(): ?int
{
// returns `null` in case size is empty (includes `0`)
// @see \TYPO3\CMS\Core\Resource\AbstractFile::getSize()
return $this->payloadFileInfo->getSize() ?: null;
}
public function getMimeType(): ?string
{
$mimeType = $this->payloadFileInfo->getMimeType();
return is_string($mimeType) ? $mimeType : null;
}
public function getContents(): string
{
return file_get_contents($this->payloadFilePath);
}
public function getSha1(): string
{
return sha1_file($this->payloadFilePath);
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
/**
* Use in `UploadedFileReferenceConverter` handling file uploads.
* `PseudoFile` and `PseudoFileReference` are independent and not associated.
*
* This facade hides (potential) internal properties from being exposed to the
* public during serialization. `sys_file_reference.uid` or `sys_file.uid` are
* the only aspects used externally. In case of missing integrity checks during
* deserialization, both properties would allow direct object reference (IDOR).
*
* @internal
*/
class PseudoFileReference extends FileReference
{
/**
* @var int|null
*/
private $_uid;
/**
* @var int|null
*/
private $_uidLocal;
public function __sleep(): array
{
// in case this is a persisted file reference, use it directly as reference
// as a consequence, in-memory changes are lost and have to be persisted first
// (it seems that in ext:form a `FileReference` was never persisted)
if ($this->uid > 0) {
$this->_uid = (int)$this->uid;
return ['_uid'];
}
if ($this->getOriginalResource()->getUid() > 0) {
$this->_uid = (int)$this->getOriginalResource()->getUid();
return ['_uid'];
}
// in case this is a transient file reference, just expose the associated `sys_file.uid`
// (based on previous comments, this is the most probably case in ext:form)
$this->_uidLocal = (int)$this->getOriginalResource()->getOriginalFile()->getUid();
return ['_uidLocal'];
}
public function __wakeup(): void
{
$factory = GeneralUtility::makeInstance(ResourceFactory::class);
if ($this->_uid > 0) {
$this->originalResource = $factory->getFileReferenceObject($this->_uid);
} elseif ($this->_uidLocal > 0) {
$this->originalResource = $factory->createFileReferenceObject([
'uid_local' => $this->_uidLocal,
'uid_foreign' => 0,
'uid' => 0,
'crop' => null,
]);
} else {
throw new \LogicException(
sprintf('Cannot unserialize %s', static::class),
1613216548
);
}
unset($this->_uid, $this->_uidLocal);
}
}
@@ -0,0 +1,377 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\File as File;
use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Property\Exception\TypeConverterException;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Form\Slot\ResourcePublicationSlot;
/**
* Class UploadedFileReferenceConverter
*
* Scope: frontend
* @internal
*/
class UploadedFileReferenceConverter extends AbstractTypeConverter
{
/**
* Folder where the file upload should go to (including storage).
*/
const CONFIGURATION_UPLOAD_FOLDER = 1;
/**
* How to handle a upload when the name of the uploaded file conflicts.
*/
const CONFIGURATION_UPLOAD_CONFLICT_MODE = 2;
/**
* Random seed to be used for deriving storage sub-folders.
*/
const CONFIGURATION_UPLOAD_SEED = 3;
/**
* Validator for file types
*/
const CONFIGURATION_FILE_VALIDATORS = 4;
/**
* @var string
*/
protected $defaultUploadFolder = '1:/user_upload/';
/**
* One of 'cancel', 'replace', 'rename'
*
* @var string
*/
protected $defaultConflictMode = 'rename';
/**
* @var array
*/
protected $sourceTypes = ['array'];
/**
* @var string
*/
protected $targetType = PseudoFileReference::class;
/**
* Take precedence over the available FileReferenceConverter
*
* @var int
*/
protected $priority = 12;
/**
* @var PseudoFileReference[]
*/
protected $convertedResources = [];
/**
* @var ResourceFactory
*/
protected $resourceFactory;
/**
* @var \TYPO3\CMS\Extbase\Security\Cryptography\HashService
*/
protected $hashService;
/**
* @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
*/
protected $persistenceManager;
/**
* @param ResourceFactory $resourceFactory
* @internal
*/
public function injectResourceFactory(ResourceFactory $resourceFactory)
{
$this->resourceFactory = $resourceFactory;
}
/**
* @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService
* @internal
*/
public function injectHashService(\TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService)
{
$this->hashService = $hashService;
}
/**
* @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
* @internal
*/
public function injectPersistenceManager(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager)
{
$this->persistenceManager = $persistenceManager;
}
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* @param array $source
* @param string $targetType
* @param array $convertedChildProperties
* @param PropertyMappingConfigurationInterface $configuration
* @return AbstractFileFolder|Error|null
* @internal
*/
public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
{
// slot/listener using `FileDumpController` instead of direct public URL in (later) rendering process
$resourcePublicationSlot = GeneralUtility::makeInstance(ResourcePublicationSlot::class);
if (!isset($source['error']) || $source['error'] === \UPLOAD_ERR_NO_FILE) {
if (isset($source['submittedFile']['resourcePointer'])) {
try {
// File references use numeric resource pointers, direct
// file relations are using "file:" prefix (e.g. "file:5")
$resourcePointer = $this->hashService->validateAndStripHmac($source['submittedFile']['resourcePointer']);
if (strpos($resourcePointer, 'file:') === 0) {
$fileUid = (int)substr($resourcePointer, 5);
$resource = $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid));
} else {
$resource = $this->createFileReferenceFromFalFileReferenceObject(
$this->resourceFactory->getFileReferenceObject($resourcePointer),
(int)$resourcePointer
);
}
$resourcePublicationSlot->add($resource->getOriginalResource()->getOriginalFile());
return $resource;
} catch (\InvalidArgumentException $e) {
// Nothing to do. No file is uploaded and resource pointer is invalid. Discard!
}
}
return null;
}
if ($source['error'] !== \UPLOAD_ERR_OK) {
return $this->objectManager->get(Error::class, $this->getUploadErrorMessage($source['error']), 1471715915);
}
if (isset($this->convertedResources[$source['tmp_name']])) {
return $this->convertedResources[$source['tmp_name']];
}
try {
$resource = $this->importUploadedResource($source, $configuration);
$resourcePublicationSlot->add($resource->getOriginalResource()->getOriginalFile());
} catch (TypeConverterException $e) {
return $e->getError();
} catch (\Exception $e) {
return $this->objectManager->get(Error::class, $e->getMessage(), $e->getCode());
}
$this->convertedResources[$source['tmp_name']] = $resource;
return $resource;
}
/**
* Import a resource and respect configuration given for properties
*
* @param array $uploadInfo
* @param PropertyMappingConfigurationInterface $configuration
* @return PseudoFileReference
*/
protected function importUploadedResource(
array $uploadInfo,
PropertyMappingConfigurationInterface $configuration
): PseudoFileReference {
if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) {
throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1471710357);
}
// `CONFIGURATION_UPLOAD_SEED` is expected to be defined
// if it's not given any random seed is generated, instead of throwing an exception
$seed = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_SEED)
?: GeneralUtility::makeInstance(Random::class)->generateRandomHexString(40);
$uploadFolderId = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $this->defaultUploadFolder;
$conflictMode = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $this->defaultConflictMode;
$pseudoFile = GeneralUtility::makeInstance(PseudoFile::class, $uploadInfo);
$validators = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_FILE_VALIDATORS);
if (is_array($validators)) {
foreach ($validators as $validator) {
if ($validator instanceof AbstractValidator) {
$validationResult = $validator->validate($pseudoFile);
if ($validationResult->hasErrors()) {
throw TypeConverterException::fromError($validationResult->getErrors()[0]);
}
}
}
}
$uploadFolder = $this->provideUploadFolder($uploadFolderId);
// current folder name, derived from public random seed (`formSession`)
$currentName = 'form_' . GeneralUtility::hmac($seed, self::class);
$uploadFolder = $this->provideTargetFolder($uploadFolder, $currentName);
// sub-folder in $uploadFolder with 160 bit of derived entropy (.../form_<40-chars-hash>/actual.file)
$uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode);
$resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) && strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === false
? (int)$this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer'])
: null;
$fileReferenceModel = $this->createFileReferenceFromFalFileObject($uploadedFile, $resourcePointer);
return $fileReferenceModel;
}
/**
* @param File $file
* @param int $resourcePointer
* @return PseudoFileReference
*/
protected function createFileReferenceFromFalFileObject(
File $file,
int $resourcePointer = null
): PseudoFileReference {
$fileReference = $this->resourceFactory->createFileReferenceObject(
[
'uid_local' => $file->getUid(),
'uid_foreign' => uniqid('NEW_'),
'uid' => uniqid('NEW_'),
'crop' => null,
]
);
return $this->createFileReferenceFromFalFileReferenceObject($fileReference, $resourcePointer);
}
/**
* In case no $resourcePointer is given a new file reference domain object
* will be returned. Otherwise the file reference is reconstituted from
* storage and will be updated(!) with the provided $falFileReference.
*
* @param CoreFileReference $falFileReference
* @param int $resourcePointer
* @return PseudoFileReference
*/
protected function createFileReferenceFromFalFileReferenceObject(
CoreFileReference $falFileReference,
int $resourcePointer = null
): PseudoFileReference {
if ($resourcePointer === null) {
$fileReference = $this->objectManager->get(PseudoFileReference::class);
} else {
$fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, PseudoFileReference::class, false);
}
$fileReference->setOriginalResource($falFileReference);
return $fileReference;
}
/**
* Returns a human-readable message for the given PHP file upload error
* constant.
*
* @param int $errorCode
* @return string
*/
protected function getUploadErrorMessage(int $errorCode): string
{
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
switch ($errorCode) {
case \UPLOAD_ERR_INI_SIZE:
$logger->error('The uploaded file exceeds the upload_max_filesize directive in php.ini.', []);
return TranslationService::getInstance()->translate('upload.error.150530345', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_FORM_SIZE:
$logger->error('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', []);
return TranslationService::getInstance()->translate('upload.error.150530345', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_PARTIAL:
$logger->error('The uploaded file was only partially uploaded.', []);
return TranslationService::getInstance()->translate('upload.error.150530346', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_NO_FILE:
$logger->error('No file was uploaded.', []);
return TranslationService::getInstance()->translate('upload.error.150530347', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_NO_TMP_DIR:
$logger->error('Missing a temporary folder.', []);
return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_CANT_WRITE:
$logger->error('Failed to write file to disk.', []);
return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
case \UPLOAD_ERR_EXTENSION:
$logger->error('File upload stopped by extension.', []);
return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
default:
$logger->error('Unknown upload error.', []);
return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf');
}
}
/**
* Ensures that upload folder exists, creates it if it does not.
*
* @param string $uploadFolderIdentifier
* @return Folder
*/
protected function provideUploadFolder(string $uploadFolderIdentifier): Folder
{
try {
return $this->resourceFactory->getFolderObjectFromCombinedIdentifier($uploadFolderIdentifier);
} catch (FolderDoesNotExistException $exception) {
[$storageId, $storagePath] = explode(':', $uploadFolderIdentifier, 2);
$storage = $this->resourceFactory->getStorageObject($storageId);
$folderNames = GeneralUtility::trimExplode('/', $storagePath, true);
$uploadFolder = $this->provideTargetFolder($storage->getRootLevelFolder(), ...$folderNames);
$this->provideFolderInitialization($uploadFolder);
return $uploadFolder;
}
}
/**
* Ensures that particular target folder exists, creates it if it does not.
*
* @param Folder $parentFolder
* @param string $folderName
* @return Folder
*/
protected function provideTargetFolder(Folder $parentFolder, string $folderName): Folder
{
return $parentFolder->hasFolder($folderName)
? $parentFolder->getSubfolder($folderName)
: $parentFolder->createFolder($folderName);
}
/**
* Creates empty index.html file to avoid directory indexing,
* in case it does not exist yet.
*
* @param Folder $parentFolder
*/
protected function provideFolderInitialization(Folder $parentFolder): void
{
if (!$parentFolder->hasFile('index.html')) {
$parentFolder->createFile('index.html');
}
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation;
/*
* This file is part of the TYPO3 CMS project.
*
* It originated from the Neos.Form package (www.neos.io)
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
/**
* Validator for countable types
*
* Scope: frontend
* @internal
*/
class CountValidator extends AbstractValidator
{
/**
* @var array
*/
protected $supportedOptions = [
'minimum' => [0, 'The minimum count to accept', 'integer'],
'maximum' => [PHP_INT_MAX, 'The maximum count to accept', 'integer']
];
/**
* The given value is valid if it is an array or \Countable that contains the specified amount of elements.
*
* @param mixed $value
*/
public function isValid($value)
{
if (!is_array($value) && !($value instanceof \Countable)) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1475002976',
'form'
),
1475002976
);
return;
}
$minimum = (int)$this->options['minimum'];
$maximum = (int)$this->options['maximum'];
if (count($value) < $minimum || count($value) > $maximum) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1475002994',
'form',
[$minimum, $maximum]
),
1475002994,
[$this->options['minimum'], $this->options['maximum']]
);
}
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for date ranges
*
* Scope: frontend
*/
class DateRangeValidator extends AbstractValidator
{
/**
* @var array
*/
protected $supportedOptions = [
'minimum' => ['', 'The minimum date formatted as Y-m-d', 'string'],
'maximum' => ['', 'The maximum date formatted as Y-m-d', 'string'],
'format' => ['Y-m-d', 'The format of the minimum and maximum option', 'string'],
];
/**
* @param \DateTime $value The value that should be validated
*/
public function isValid($value)
{
$this->validateOptions();
if (!($value instanceof \DateTime)) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1521293685',
'form',
[gettype($value)]
),
1521293685
);
return;
}
$minimum = $this->options['minimum'];
$maximum = $this->options['maximum'];
$format = $this->options['format'];
$value->modify('midnight');
if (
$minimum instanceof \DateTime
&& $value < $minimum
) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1521293686',
'form',
[$minimum->format($format)]
),
1521293686,
[$minimum->format($format)]
);
}
if (
$maximum instanceof \DateTime
&& $value > $maximum
) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1521293687',
'form',
[$maximum->format($format)]
),
1521293687,
[$maximum->format($format)]
);
}
}
/**
* Checks if this validator is correctly configured
*
* @throws InvalidValidationOptionsException if the configured validation options are incorrect
*/
protected function validateOptions()
{
if (!empty($this->options['minimum'])) {
$minimum = \DateTime::createFromFormat($this->options['format'], $this->options['minimum']);
if (!($minimum instanceof \DateTime)) {
$message = sprintf('The option "minimum" (%s) could not be converted to \DateTime from format "%s".', $this->options['minimum'], $this->options['format']);
throw new InvalidValidationOptionsException($message, 1521293813);
}
$minimum->modify('midnight');
$this->options['minimum'] = $minimum;
}
if (!empty($this->options['maximum'])) {
$maximum = \DateTime::createFromFormat($this->options['format'], $this->options['maximum']);
if (!($maximum instanceof \DateTime)) {
$message = sprintf('The option "maximum" (%s) could not be converted to \DateTime from format "%s".', $this->options['maximum'], $this->options['format']);
throw new InvalidValidationOptionsException($message, 1521293814);
}
$maximum->modify('midnight');
$this->options['maximum'] = $maximum;
}
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
/**
* Validator for empty values.
*
* Scope: frontend
*/
class EmptyValidator extends AbstractValidator
{
/**
* This validator always needs to be executed even if the given value is empty.
* See AbstractValidator::validate()
*
* @var bool
*/
protected $acceptsEmptyValues = true;
/**
* Checks if the given property ($propertyValue) is empty (NULL, empty string, empty array or empty object).
*
* @param mixed $value The value that should be validated
*/
public function isValid($value)
{
if (!empty($value)) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1476396435',
'form'
),
1476396435
);
}
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation\Exception;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Form\Exception;
/**
*/
class InvalidValidationOptionsException extends Exception
{
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Property\TypeConverter\PseudoFile;
use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for countable types
*
* Scope: frontend
* @internal
*/
class FileSizeValidator extends AbstractValidator
{
/**
* @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']
];
/**
* The given value is valid
*
* @param FileReference|File|PseudoFile $resource
*/
public function isValid($resource)
{
$this->validateOptions();
if ($resource instanceof FileReference) {
$fileSize = $resource->getOriginalResource()->getSize();
} elseif ($resource instanceof File) {
$fileSize = $resource->getSize();
} elseif ($resource instanceof PseudoFile) {
$fileSize = $resource->getSize();
} else {
$this->addError(
$this->translateErrorMessage(
'validation.error.1505303626',
'form'
),
1505303626
);
return;
}
$minFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['minimum']);
$maxFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['maximum']);
$labels = ' Bytes| Kilobyte| Megabyte| Gigabyte';
if ($fileSize < $minFileSize) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1505305752',
'form',
[GeneralUtility::formatSize($minFileSize, $labels)]
),
1505305752,
[GeneralUtility::formatSize($minFileSize, $labels)]
);
}
if ($fileSize > $maxFileSize) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1505305753',
'form',
[GeneralUtility::formatSize($maxFileSize, $labels)]
),
1505305753,
[GeneralUtility::formatSize($maxFileSize, $labels)]
);
}
}
/**
* Checks if this validator is correctly configured
*
* @throws InvalidValidationOptionsException if the configured validation options are incorrect
*/
protected function validateOptions()
{
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".', 1505304205);
}
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".', 1505304206);
}
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Mvc\Validation;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\MimeTypeDetector;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Property\TypeConverter\PseudoFile;
use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException;
/**
* Validator for mime types
*
* Scope: frontend
*/
class MimeTypeValidator extends AbstractValidator
{
/**
* @var array
*/
protected $supportedOptions = [
'allowedMimeTypes' => [null, 'Allowed mime types (using */* IANA media types)', 'array', true]
];
/**
* The given $value is valid if it is a FileReference of the
* configured type (one of the IANA media types)
*
* Note: a value of NULL or empty string ('') is considered valid
*
* @param FileReference|File|PseudoFile $resource The resource that should be validated
*/
public function isValid($resource)
{
$this->validateOptions();
if ($resource instanceof FileReference) {
$mimeType = $resource->getOriginalResource()->getMimeType();
$fileExtension = $resource->getOriginalResource()->getExtension();
} elseif ($resource instanceof File) {
$mimeType = $resource->getMimeType();
$fileExtension = $resource->getExtension();
} elseif ($resource instanceof PseudoFile) {
$mimeType = $resource->getMimeType();
$fileExtension = $resource->getExtension();
} else {
$this->addError(
$this->translateErrorMessage(
'validation.error.1471708997',
'form'
),
1471708997
);
return;
}
$allowedMimeTypes = $this->options['allowedMimeTypes'];
if (!in_array($mimeType, $allowedMimeTypes, true)) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1471708998',
'form',
[$mimeType]
),
1471708998,
[$mimeType]
);
} else {
// The mime-type which was detected by FAL matches, but the file name does not match.
// 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. The file extension of the uploaded file must match
// the mime-type for this file.
$assumedMimesTypeOfFileExtension = (new MimeTypeDetector())->getMimeTypesForFileExtension($fileExtension);
if (empty(array_intersect($allowedMimeTypes, $assumedMimesTypeOfFileExtension))) {
$this->addError(
$this->translateErrorMessage(
'validation.error.1613126216',
'form',
[$fileExtension]
) ?? '',
1613126216,
[$fileExtension]
);
}
}
}
/**
* Checks if this validator is correctly configured
*
* @throws InvalidValidationOptionsException if the configured validation options are incorrect
*/
protected function validateOptions()
{
if (!is_array($this->options['allowedMimeTypes']) || $this->options['allowedMimeTypes'] === []) {
throw new InvalidValidationOptionsException('The option "allowedMimeTypes" must be an array with at least one item.', 1471713296);
}
}
}