TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
<?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\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
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\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||
|
||||
/**
|
||||
* A PSR-14 event listener for various FAL related functionality.
|
||||
*
|
||||
* @internal
|
||||
* @deprecated: Remove in v16 along with the FileFormsToDatabaseUpgradeWizard
|
||||
*/
|
||||
final class FilePersistenceSlot implements SingletonInterface
|
||||
{
|
||||
public const string COMMAND_FILE_ADD = 'fileAdd';
|
||||
public const string COMMAND_FILE_CREATE = 'fileCreate';
|
||||
public const string COMMAND_FILE_MOVE = 'fileMove';
|
||||
public const string COMMAND_FILE_RENAME = 'fileRename';
|
||||
public const string COMMAND_FILE_REPLACE = 'fileReplace';
|
||||
public const string COMMAND_FILE_SET_CONTENTS = 'fileSetContents';
|
||||
|
||||
private array $allowedInvocations = [];
|
||||
|
||||
public function __construct(private readonly HashService $hashService) {}
|
||||
|
||||
public function getContentSignature(string $content): string
|
||||
{
|
||||
return $this->hashService->hmac($content, self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows invocation for a particular combination of command and file
|
||||
* identifier. Commands providing new content 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;
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/creation')]
|
||||
public function onPreFileCreate(BeforeFileCreatedEvent $event): void
|
||||
{
|
||||
$combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFolder(), $event->getFileName());
|
||||
$this->assertFileName(self::COMMAND_FILE_CREATE, $combinedFileIdentifier);
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/add')]
|
||||
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 not 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()));
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/rename')]
|
||||
public function onPreFileRename(BeforeFileRenamedEvent $event): void
|
||||
{
|
||||
$combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFile()->getParentFolder(), $event->getTargetFileName() ?? '');
|
||||
$this->assertFileName(self::COMMAND_FILE_RENAME, $combinedFileIdentifier);
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/replace')]
|
||||
public function onPreFileReplace(BeforeFileReplacedEvent $event): void
|
||||
{
|
||||
$combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFile()->getParentFolder(), $event->getFile()->getName());
|
||||
$this->assertFileName(self::COMMAND_FILE_REPLACE, $combinedFileIdentifier);
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/move')]
|
||||
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);
|
||||
}
|
||||
|
||||
#[AsEventListener('form-framework/update-content')]
|
||||
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
|
||||
*/
|
||||
private function assertFileName(string $command, string $combinedFileIdentifier, ?string $content = null): void
|
||||
{
|
||||
if (!$this->isFormDefinition($combinedFileIdentifier)) {
|
||||
return;
|
||||
}
|
||||
$contentSignature = null;
|
||||
if ($content !== null) {
|
||||
$contentSignature = $this->getContentSignature($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]);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
|
||||
{
|
||||
return sprintf('%d:%s%s', $folder->getStorage()->getUid(), $folder->getIdentifier(), $fileName);
|
||||
}
|
||||
|
||||
private function isFormDefinition(string $identifier): bool
|
||||
{
|
||||
return str_ends_with(
|
||||
mb_strtolower($identifier),
|
||||
FormPersistenceManagerInterface::FORM_DEFINITION_FILE_EXTENSION
|
||||
);
|
||||
}
|
||||
|
||||
private function isRecycleFolder(FolderInterface $folder): bool
|
||||
{
|
||||
$role = $folder->getStorage()->getRole($folder);
|
||||
return $role === FolderInterface::ROLE_RECYCLER;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user