Files
cms-form/Classes/Hooks/FormDefinitionDataHandlerHook.php

128 lines
4.5 KiB
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\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Form\Storage\Security\FormDefinitionPersistenceCommand;
use TYPO3\CMS\Form\Storage\Security\FormDefinitionPersistenceGuard;
/**
* Denies direct DataHandler write access to the form_definition table unless
* FormDefinitionPersistenceGuard has granted a matching invocation. This
* ensures form definitions can only be persisted through DatabaseStorageAdapter,
* which applies the form persistence manager's validation and permission checks.
*
* Registered as processDatamapClass (create/update) and processCmdmapClass
* (delete) so each path carries command, identifier, and field-level checks.
*
* @internal
*/
#[Autoconfigure(public: true)]
final readonly class FormDefinitionDataHandlerHook
{
public function __construct(
private FormDefinitionPersistenceGuard $guard,
) {}
/**
* Verifies create/update operations against the pending invocation grant.
*
* The command is derived from $id: a NEW-prefixed string means create,
* an integer means update. The full incoming field array is matched against
* the stored field names and HMAC. Setting $incomingFieldArray to null
* cancels the record in DataHandler.
*
* @param array<string, mixed>|null $incomingFieldArray
* @param-out array<string, mixed>|null $incomingFieldArray
*/
public function processDatamap_preProcessFieldArray(
?array &$incomingFieldArray,
string $table,
string|int $id,
DataHandler $dataHandler,
): void {
if ($table !== 'form_definition') {
return;
}
$isUpdate = MathUtility::canBeInterpretedAsInteger($id);
$command = $isUpdate
? FormDefinitionPersistenceCommand::Update
: FormDefinitionPersistenceCommand::Create;
$recordIdentifier = $isUpdate ? (int)$id : (string)$id;
if (!$this->guard->isInvocationAllowed($command, $recordIdentifier, $incomingFieldArray)) {
$incomingFieldArray = null;
$dataHandler->log(
$table,
$isUpdate ? (int)$id : 0,
$isUpdate ? SystemLogDatabaseAction::UPDATE : SystemLogDatabaseAction::INSERT,
null,
SystemLogErrorClassification::USER_ERROR,
'Persisting form definition "%s" via DataHandler is denied',
null,
[$id],
);
return;
}
$this->guard->consumeInvocation($command, $recordIdentifier, $incomingFieldArray);
}
/**
* Blocks unauthorised delete commands on form_definition records.
*
* Sets $commandIsProcessed to true (preventing DataHandler's built-in
* delete) when no COMMAND_FORM_DELETE grant is pending. This mirrors the
* FilePersistenceSlot pattern for FAL-based form definitions.
*/
public function processCmdmap(
string $command,
string $table,
int|string $id,
mixed $value,
bool &$commandIsProcessed,
DataHandler $dataHandler,
): void {
if ($table !== 'form_definition' || $command !== 'delete') {
return;
}
if (!$this->guard->isInvocationAllowed(FormDefinitionPersistenceCommand::Delete, (int)$id)) {
$commandIsProcessed = true;
$dataHandler->log(
$table,
(int)$id,
SystemLogDatabaseAction::DELETE,
null,
SystemLogErrorClassification::USER_ERROR,
'Deleting form definition "%s" via DataHandler is denied',
null,
[$id],
);
return;
}
$this->guard->consumeInvocation(FormDefinitionPersistenceCommand::Delete, (int)$id);
}
}