first commit
This commit is contained in:
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
|
||||
{
|
||||
}
|
||||
+28
@@ -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
|
||||
{
|
||||
}
|
||||
+25
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+208
@@ -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);
|
||||
}
|
||||
}
|
||||
+377
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user