Files
VITEC-website/packages/vitec/Classes/Middleware/FormSubmissionMiddleware.php

171 lines
6.5 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Middleware;
use Evomedien\Vitec\Forms\Delivery\DeliveryInterface;
use Evomedien\Vitec\Forms\Delivery\EmailDelivery;
use Evomedien\Vitec\Forms\Delivery\SalesforceDelivery;
use Evomedien\Vitec\Forms\FormDefinitions;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Headless form endpoint: POST /api/vitec/form/<formKey>
*
* Accepts JSON (or form-encoded) submissions from the React frontend for
* the VITEC form plugins (contact | demo | helpdesk):
*
* 1. honeypot check (silently accepted, nothing stored)
* 2. server-side validation against FormDefinitions (single source of truth)
* 3. submission stored in tx_vitec_form_submission (backup log)
* 4. delivery via strategy: email (active) or salesforce (prepared stub) —
* configured in the FlexForm of the form's plugin element
*
* A stored-but-undelivered submission still responds success:true — the
* data is safe in the log and the delivery status is visible in the backend.
*/
final class FormSubmissionMiddleware implements MiddlewareInterface
{
private const PATH_PATTERN = '#^/api/vitec/form/([a-z]+)/?$#';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$path = $request->getUri()->getPath();
if (preg_match(self::PATH_PATTERN, $path, $m) !== 1) {
return $handler->handle($request);
}
$formKey = $m[1];
if (strtoupper($request->getMethod()) !== 'POST') {
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'POST only']], 405);
}
if (FormDefinitions::get($formKey) === null) {
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unknown form']], 404);
}
try {
$payload = $this->parseBody($request);
// Honeypot: pretend success, store nothing.
if (trim((string)($payload[FormDefinitions::HONEYPOT_FIELD] ?? '')) !== '') {
return new JsonResponse(['success' => true]);
}
$errors = FormDefinitions::validate($formKey, $payload);
if ($errors !== []) {
return new JsonResponse(['success' => false, 'errors' => $errors], 422);
}
$clean = FormDefinitions::filterPayload($formKey, $payload);
$settings = $this->pluginSettings($formKey);
$method = (string)($settings['delivery'] ?? 'email');
$submissionUid = $this->store($formKey, $clean, $method);
try {
$this->delivery($method)->deliver($formKey, $clean, $settings);
$this->updateStatus($submissionUid, 'sent', '');
} catch (\Throwable $e) {
$this->updateStatus($submissionUid, 'failed', $e->getMessage());
}
// Data is safely logged either way.
return new JsonResponse(['success' => true]);
} catch (\Throwable $e) {
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unexpected error']], 500);
}
}
/** @return array<string,mixed> */
private function parseBody(ServerRequestInterface $request): array
{
$contentType = $request->getHeaderLine('Content-Type');
if (str_contains($contentType, 'application/json')) {
$decoded = json_decode((string)$request->getBody(), true);
return is_array($decoded) ? $decoded : [];
}
$parsed = $request->getParsedBody();
return is_array($parsed) ? $parsed : [];
}
/**
* Delivery settings come from the FlexForm of the (first) plugin element
* of this form type — one configuration per form type.
*
* @return array<string,mixed>
*/
private function pluginSettings(string $formKey): array
{
try {
$ctype = array_search($formKey, FormDefinitions::CTYPE_MAP, true);
if ($ctype === false) {
return [];
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$row = $qb
->select('pi_flexform')
->from('tt_content')
->where(
$qb->expr()->eq('CType', $qb->createNamedParameter($ctype)),
$qb->expr()->eq('deleted', 0),
$qb->expr()->eq('hidden', 0)
)
->orderBy('uid', 'ASC')
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
if (!$row) {
return [];
}
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$data = $flexFormService->convertFlexFormContentToArray((string)$row['pi_flexform']);
return $data['settings'] ?? [];
} catch (\Throwable $e) {
return [];
}
}
private function delivery(string $method): DeliveryInterface
{
return $method === 'salesforce'
? GeneralUtility::makeInstance(SalesforceDelivery::class)
: GeneralUtility::makeInstance(EmailDelivery::class);
}
/** @param array<string,string> $payload */
private function store(string $formKey, array $payload, string $method): int
{
$conn = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_vitec_form_submission');
$conn->insert('tx_vitec_form_submission', [
'pid' => 0,
'crdate' => time(),
'tstamp' => time(),
'form_key' => $formKey,
'payload' => (string)json_encode($payload, JSON_UNESCAPED_UNICODE),
'delivery_method' => $method,
'delivery_status' => 'pending',
]);
return (int)$conn->lastInsertId();
}
private function updateStatus(int $uid, string $status, string $error): void
{
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_vitec_form_submission')
->update(
'tx_vitec_form_submission',
['delivery_status' => $status, 'delivery_error' => mb_substr($error, 0, 1000), 'tstamp' => time()],
['uid' => $uid]
);
}
}