| Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-form/Classes/Slot/ |
| Current File : /var/www/surf/TYPO3/vendor/typo3/cms-form/Classes/Slot/FilePersistenceSlot.php |
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Form\Slot;
use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent;
use TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent;
use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManager;
/**
* A PSR-14 event listener for various FAL related functionality.
*
* @internal will be renamed at some point.
*/
final class FilePersistenceSlot implements SingletonInterface
{
public const COMMAND_FILE_ADD = 'fileAdd';
public const COMMAND_FILE_CREATE = 'fileCreate';
public const COMMAND_FILE_MOVE = 'fileMove';
public const COMMAND_FILE_RENAME = 'fileRename';
public const COMMAND_FILE_REPLACE = 'fileReplace';
public const COMMAND_FILE_SET_CONTENTS = 'fileSetContents';
/**
* @var array
*/
protected $definedInvocations = [];
/**
* @var array
*/
protected $allowedInvocations = [];
public function getContentSignature(string $content): string
{
return GeneralUtility::hmac($content);
}
/**
* Defines invocations on command level only depending on the type:
*
* + true: whitelist command, takes precedence over $allowedInvocations
* + false: blacklist command, takes precedence over $allowedInvocations
* + removes previously definition for particular command
*
* @param string $command
* @param bool|null $type
*/
public function defineInvocation(string $command, bool $type = null)
{
$this->definedInvocations[$command] = $type;
if ($type === null) {
unset($this->definedInvocations[$command]);
}
}
/**
* Allows invocation for a particular combination of command and file
* identifier. Commands providing new content have have to submit a HMAC
* signature on the content as well.
*
* @see getContentSignature
*/
public function allowInvocation(
string $command,
string $combinedFileIdentifier,
string $contentSignature = null
): bool {
$index = $this->searchAllowedInvocation(
$command,
$combinedFileIdentifier,
$contentSignature
);
if ($index !== null) {
return false;
}
$this->allowedInvocations[] = [
'command' => $command,
'combinedFileIdentifier' => $combinedFileIdentifier,
'contentSignature' => $contentSignature,
];
return true;
}
public function onPreFileCreate(BeforeFileCreatedEvent $event): void
{
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getFolder(),
$event->getFileName()
);
$this->assertFileName(
self::COMMAND_FILE_CREATE,
$combinedFileIdentifier
);
}
public function onPreFileAdd(BeforeFileAddedEvent $event): void
{
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getTargetFolder(),
$event->getFileName()
);
// while assertFileName below also checks if it's a form definition
// we want an early return here to get rid of the file_get_contents
// below which would be triggered on every file add command otherwise
if (!$this->isFormDefinition($combinedFileIdentifier)) {
return;
}
$this->assertFileName(
self::COMMAND_FILE_ADD,
$combinedFileIdentifier,
(string)file_get_contents($event->getSourceFilePath())
);
}
public function onPreFileRename(BeforeFileRenamedEvent $event): void
{
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getFile()->getParentFolder(),
$event->getTargetFileName() ?? ''
);
$this->assertFileName(
self::COMMAND_FILE_RENAME,
$combinedFileIdentifier
);
}
public function onPreFileReplace(BeforeFileReplacedEvent $event): void
{
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getFile()->getParentFolder(),
$event->getFile()->getName()
);
$this->assertFileName(
self::COMMAND_FILE_REPLACE,
$combinedFileIdentifier
);
}
public function onPreFileMove(BeforeFileMovedEvent $event): void
{
// Skip check, in case file extension would not change during this
// command. In case e.g. "file.txt" shall be renamed to "file.form.yaml"
// the invocation still has to be granted.
// Any file moved to a recycle folder is accepted as well.
if ($this->isFormDefinition($event->getFile()->getIdentifier())
&& $this->isFormDefinition($event->getTargetFileName())
|| $this->isRecycleFolder($event->getFolder())) {
return;
}
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getFolder(),
$event->getTargetFileName()
);
$this->assertFileName(
self::COMMAND_FILE_MOVE,
$combinedFileIdentifier
);
}
public function onPreFileSetContents(BeforeFileContentsSetEvent $event): void
{
$combinedFileIdentifier = $this->buildCombinedIdentifier(
$event->getFile()->getParentFolder(),
$event->getFile()->getName()
);
$this->assertFileName(
self::COMMAND_FILE_SET_CONTENTS,
$combinedFileIdentifier,
$event->getContent()
);
}
/**
* @throws FormDefinitionPersistenceException
*/
protected function assertFileName(
string $command,
string $combinedFileIdentifier,
string $content = null
): void {
if (!$this->isFormDefinition($combinedFileIdentifier)) {
return;
}
$definedInvocation = $this->definedInvocations[$command] ?? null;
// whitelisted command
if ($definedInvocation === true) {
return;
}
// blacklisted command
if ($definedInvocation === false) {
throw new FormDefinitionPersistenceException(
sprintf(
'Persisting form definition "%s" is denied',
$combinedFileIdentifier
),
1530281201
);
}
$contentSignature = null;
if ($content !== null) {
$contentSignature = $this->getContentSignature((string)$content);
}
$allowedInvocationIndex = $this->searchAllowedInvocation(
$command,
$combinedFileIdentifier,
$contentSignature
);
if ($allowedInvocationIndex === null) {
throw new FormDefinitionPersistenceException(
sprintf(
'Persisting form definition "%s" is denied',
$combinedFileIdentifier
),
1530281202
);
}
unset($this->allowedInvocations[$allowedInvocationIndex]);
}
/**
* @param string|null $contentSignature
*/
protected function searchAllowedInvocation(
string $command,
string $combinedFileIdentifier,
string $contentSignature = null
): ?int {
foreach ($this->allowedInvocations as $index => $allowedInvocation) {
if (
$command === $allowedInvocation['command']
&& $combinedFileIdentifier === $allowedInvocation['combinedFileIdentifier']
&& $contentSignature === $allowedInvocation['contentSignature']
) {
return $index;
}
}
return null;
}
protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
{
return sprintf(
'%d:%s%s',
$folder->getStorage()->getUid(),
$folder->getIdentifier(),
$fileName
);
}
protected function isFormDefinition(string $identifier): bool
{
return str_ends_with(
$identifier,
FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION
);
}
protected function isRecycleFolder(FolderInterface $folder): bool
{
$role = $folder->getStorage()->getRole($folder);
return $role === FolderInterface::ROLE_RECYCLER;
}
}