TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:24 +02:00
commit aad9daaefd
1506 changed files with 94005 additions and 0 deletions
+187
View File
@@ -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;
}
}
@@ -0,0 +1,24 @@
<?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;
/**
* @internal
* @deprecated: Remove in v16 along with the FileFormsToDatabaseUpgradeWizard
*/
class FormDefinitionPersistenceException extends \RuntimeException {}
+92
View File
@@ -0,0 +1,92 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* A PSR-14 event listener for using FAL resources in public (frontend)
*
* @internal will be renamed at some point.
*/
class ResourcePublicationSlot implements SingletonInterface
{
/**
* @var list<string>
*/
protected $fileIdentifiers = [];
public function __construct(private readonly HashService $hashService) {}
#[AsEventListener('form-framework/resource-getPublicUrl')]
public function getPublicUrl(GeneratePublicUrlForResourceEvent $event): void
{
$resource = $event->getResource();
if (!$resource instanceof FileInterface
|| !$this->has($resource)
|| $event->getStorage()->getDriverType() !== 'Local'
) {
return;
}
$event->setPublicUrl($this->getStreamUrl($event->getResource()));
}
public function add(FileInterface $resource): void
{
if ($this->has($resource)) {
return;
}
$this->fileIdentifiers[] = $resource->getIdentifier();
}
public function has(FileInterface $resource): bool
{
return in_array($resource->getIdentifier(), $this->fileIdentifiers, true);
}
protected function getStreamUrl(ResourceInterface $resource): string
{
$queryParameterArray = ['eID' => 'dumpFile', 't' => ''];
if ($resource instanceof File) {
$queryParameterArray['f'] = $resource->getUid();
$queryParameterArray['t'] = 'f';
} elseif ($resource instanceof ProcessedFile) {
$queryParameterArray['p'] = $resource->getUid();
$queryParameterArray['t'] = 'p';
}
$queryParameterArray['token'] = $this->hashService->hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
$publicUrl = '';
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface) {
$publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'), $GLOBALS['TYPO3_REQUEST']);
}
$publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
return $publicUrl;
}
}