TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
|
||||
use TYPO3\CMS\Form\Domain\DTO\PersistenceManagerConfiguration;
|
||||
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
|
||||
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
|
||||
use TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent;
|
||||
use TYPO3\CMS\Form\Exception;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||
use TYPO3\CMS\Form\Service\DatabaseService;
|
||||
use TYPO3\CMS\Form\Service\FormEditorEnrichmentService;
|
||||
use TYPO3\CMS\Form\Service\TranslationService;
|
||||
use TYPO3\CMS\Form\Type\FormDefinitionArray;
|
||||
use TYPO3\CMS\Form\Utility\DateRangeValidatorPatterns;
|
||||
|
||||
/**
|
||||
* The form editor controller
|
||||
*
|
||||
* Scope: backend
|
||||
* @internal
|
||||
*/
|
||||
class FormEditorController extends ActionController
|
||||
{
|
||||
use AllowedMethodsTrait;
|
||||
|
||||
protected const JS_MODULE_NAMES = ['app', 'mediator', 'viewModel'];
|
||||
|
||||
public function __construct(
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected readonly PageRenderer $pageRenderer,
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly FormDefinitionConversionService $formDefinitionConversionService,
|
||||
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||
protected readonly ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||
protected readonly TranslationService $translationService,
|
||||
protected readonly ConfigurationService $configurationService,
|
||||
protected readonly UriBuilder $coreUriBuilder,
|
||||
protected readonly ArrayFormFactory $arrayFormFactory,
|
||||
protected readonly ViewFactoryInterface $viewFactory,
|
||||
protected readonly DatabaseService $databaseService,
|
||||
protected readonly CacheManager $cacheManager,
|
||||
protected readonly ComponentFactory $componentFactory,
|
||||
protected readonly FormEditorEnrichmentService $formEditorEnrichmentService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display the form editor.
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
protected function indexAction(string $formPersistenceIdentifier = '', ?string $prototypeName = null, string $returnUrl = ''): ResponseInterface
|
||||
{
|
||||
if ($formPersistenceIdentifier === '') {
|
||||
return new RedirectResponse((string)$this->coreUriBuilder->buildUriFromRoute('form_manager'));
|
||||
}
|
||||
$formSettings = $this->getFormSettings();
|
||||
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Read "%s" is not allowed', $formPersistenceIdentifier), 1614500662);
|
||||
}
|
||||
if (PathUtility::isExtensionPath($formPersistenceIdentifier)
|
||||
&& !PersistenceManagerConfiguration::fromArray($formSettings['persistenceManager'] ?? [])->allowSaveToExtensionPaths
|
||||
) {
|
||||
throw new PersistenceManagerException('Edit an extension formDefinition is not allowed.', 1478265661);
|
||||
}
|
||||
$formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
|
||||
if ($prototypeName === null) {
|
||||
$prototypeName = $formDefinition['prototypeName'] ?? 'standard';
|
||||
} else {
|
||||
// Loading a form definition with another prototype is currently not implemented but is planned in the future.
|
||||
// This safety check is a preventive measure.
|
||||
$selectablePrototypeNames = $this->configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
|
||||
if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
|
||||
throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
|
||||
}
|
||||
}
|
||||
$formDefinition['prototypeName'] = $prototypeName;
|
||||
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($prototypeName);
|
||||
$formDefinition = $this->transformFormDefinitionForFormEditor($prototypeConfiguration, $formDefinition, $formPersistenceIdentifier);
|
||||
$formEditorDefinitions = $this->getFormEditorDefinitions($prototypeConfiguration);
|
||||
$additionalViewModelJavaScriptModules = array_map(
|
||||
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||
$prototypeConfiguration['formEditor']['dynamicJavaScriptModules']['additionalViewModelModules'] ?? []
|
||||
);
|
||||
array_map($this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $additionalViewModelJavaScriptModules);
|
||||
$formEditorAppInitialData = [
|
||||
'formEditorDefinitions' => $formEditorDefinitions,
|
||||
'formDefinition' => $formDefinition,
|
||||
'formPersistenceIdentifier' => $formPersistenceIdentifier,
|
||||
'prototypeName' => $prototypeName,
|
||||
'endpoints' => [
|
||||
'formPageRenderer' => $this->uriBuilder->uriFor('renderFormPage'),
|
||||
'saveForm' => $this->uriBuilder->uriFor('saveForm'),
|
||||
],
|
||||
'additionalViewModelModules' => $additionalViewModelJavaScriptModules,
|
||||
'maximumUndoSteps' => $prototypeConfiguration['formEditor']['maximumUndoSteps'],
|
||||
];
|
||||
$moduleTemplate = $this->initializeModuleTemplate($this->request, $returnUrl);
|
||||
$moduleTemplate->assign('formEditorTemplates', $this->renderFormEditorTemplates($prototypeConfiguration, $formEditorDefinitions));
|
||||
$moduleTemplate->getDocHeaderComponent()->addBreadcrumbSuffixNode(new BreadcrumbNode(
|
||||
identifier: $formPersistenceIdentifier,
|
||||
label: $formDefinition['label'],
|
||||
icon: 'content-form',
|
||||
));
|
||||
$addInlineSettings = [
|
||||
'FormEditor' => [
|
||||
'typo3WinBrowserUrl' => (string)$this->coreUriBuilder->buildUriFromRoute('wizard_element_browser'),
|
||||
'dateEditor' => [
|
||||
'absolutePattern' => DateRangeValidatorPatterns::RFC3339_FULL_DATE,
|
||||
],
|
||||
],
|
||||
];
|
||||
$addInlineSettings = array_replace_recursive(
|
||||
$addInlineSettings,
|
||||
$prototypeConfiguration['formEditor']['addInlineSettings']
|
||||
);
|
||||
if (json_encode($formEditorAppInitialData) === false) {
|
||||
throw new Exception('The form editor app data could not be encoded', 1628677079);
|
||||
}
|
||||
$javaScriptModules = array_map(
|
||||
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||
array_filter(
|
||||
$prototypeConfiguration['formEditor']['dynamicJavaScriptModules'] ?? [],
|
||||
fn(string $name) => in_array($name, self::JS_MODULE_NAMES, true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
)
|
||||
);
|
||||
$pageRenderer = $this->pageRenderer;
|
||||
$pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||
JavaScriptModuleInstruction::create('@typo3/form/backend/helper.js', 'Helper')
|
||||
->invoke('dispatchFormEditor', $javaScriptModules, $formEditorAppInitialData)
|
||||
);
|
||||
array_map($pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $javaScriptModules);
|
||||
$pageRenderer->addInlineSettingArray('', $addInlineSettings);
|
||||
$stylesheets = $prototypeConfiguration['formEditor']['stylesheets'];
|
||||
foreach ($stylesheets as $stylesheet) {
|
||||
$pageRenderer->addCssFile($stylesheet);
|
||||
}
|
||||
$moduleTemplate->setModuleClass($this->request->getPluginName() . '_' . $this->request->getControllerName());
|
||||
$moduleTemplate->setFlashMessageQueue($this->getFlashMessageQueue());
|
||||
$moduleTemplate->setTitle(
|
||||
$this->getLanguageService()->translate('title', 'form.module'),
|
||||
$formDefinition['label']
|
||||
);
|
||||
return $moduleTemplate->renderResponse('Backend/FormEditor/Index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the save action.
|
||||
* This action uses the Fluid JsonView::class as view.
|
||||
*/
|
||||
protected function initializeSaveFormAction(): void
|
||||
{
|
||||
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||
$this->defaultViewObjectName = JsonView::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a formDefinition which was build by the form editor.
|
||||
*/
|
||||
protected function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition): ResponseInterface
|
||||
{
|
||||
$formDefinition = $formDefinition->getArrayCopy();
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeFormIsSavedEvent($formPersistenceIdentifier, $formDefinition, $this->request),
|
||||
);
|
||||
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||
$formDefinition = $event->form;
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
];
|
||||
try {
|
||||
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Save "%s" is not allowed', $formPersistenceIdentifier), 1614500663);
|
||||
}
|
||||
$this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition, []);
|
||||
$this->flushPageCache($formPersistenceIdentifier);
|
||||
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
|
||||
$formDefinition = $this->transformFormDefinitionForFormEditor($prototypeConfiguration, $formDefinition, $formPersistenceIdentifier);
|
||||
$response['formDefinition'] = $formDefinition;
|
||||
} catch (PersistenceManagerException $e) {
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
// saveFormAction uses the extbase JsonView::class.
|
||||
// That's why we have to set the view variables in this way.
|
||||
/** @var JsonView $view */
|
||||
$view = $this->view;
|
||||
$view->assign('response', $response);
|
||||
$view->setVariablesToRender([
|
||||
'response',
|
||||
]);
|
||||
return $this->jsonResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a page from the formDefinition which was build by the form editor.
|
||||
* Use the frontend rendering and set the form framework to preview mode.
|
||||
*/
|
||||
protected function renderFormPageAction(
|
||||
FormDefinitionArray $formDefinition,
|
||||
int $pageIndex,
|
||||
?string $prototypeName = null,
|
||||
?string $formPersistenceIdentifier = null
|
||||
): ResponseInterface {
|
||||
$prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
|
||||
$formDefinition = $formDefinition->getArrayCopy();
|
||||
$formDefinition['renderingOptions']['previewMode'] = true;
|
||||
$formDefinition = $this->arrayFormFactory->build($formDefinition, $prototypeName, $this->request);
|
||||
|
||||
if ($formPersistenceIdentifier !== null) {
|
||||
$formDefinition->setRenderingOption('formPersistenceIdentifier', $formPersistenceIdentifier);
|
||||
}
|
||||
|
||||
$form = $formDefinition->bind($this->request);
|
||||
$form->setCurrentSiteLanguage($this->buildFakeSiteLanguage(0, 0));
|
||||
$form->overrideCurrentPage($pageIndex);
|
||||
return $this->htmlResponse($form->render());
|
||||
}
|
||||
|
||||
protected function getFormSettings(): array
|
||||
{
|
||||
$typoScriptSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||
$formSettings = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, false);
|
||||
if (!isset($formSettings['formManager'])) {
|
||||
// Config sub array formManager is crucial and should always exist. If it does
|
||||
// not, this indicates an issue in config loading logic. Except in this case.
|
||||
throw new \LogicException('Configuration could not be loaded', 1681549038);
|
||||
}
|
||||
return $formSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a SiteLanguage object to render the form preview with a
|
||||
* specific language.
|
||||
*/
|
||||
protected function buildFakeSiteLanguage(int $pageId, int $languageId): SiteLanguage
|
||||
{
|
||||
$fakeSiteConfiguration = [
|
||||
'languages' => [
|
||||
[
|
||||
'languageId' => $languageId,
|
||||
'title' => 'Dummy',
|
||||
'navigationTitle' => '',
|
||||
'flag' => '',
|
||||
'locale' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
return GeneralUtility::makeInstance(Site::class, 'form-dummy', $pageId, $fakeSiteConfiguration)->getLanguageById($languageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the formElements.*.formEditor section from the YAML settings.
|
||||
* Sort all formElements into groups and add additional data.
|
||||
*/
|
||||
protected function getInsertRenderablesPanelConfiguration(array $prototypeConfiguration, array $formElementsDefinition, bool $isInsertPages = false): array
|
||||
{
|
||||
/** @var array<string, list<array<string, array{key: string, cssKey: string, label: string, description: string, sorting: int, iconIdentifier: string}>>> $formElementsByGroup */
|
||||
$formElementsByGroup = [];
|
||||
foreach ($formElementsDefinition as $formElementName => $formElementConfiguration) {
|
||||
if (!isset($formElementConfiguration['group']) || ($isInsertPages && $formElementConfiguration['group'] !== 'page') || (!$isInsertPages && $formElementConfiguration['group'] === 'page')) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($formElementsByGroup[$formElementConfiguration['group']])) {
|
||||
$formElementsByGroup[$formElementConfiguration['group']] = [];
|
||||
}
|
||||
$formElementConfiguration = $this->translationService->translateValuesRecursive(
|
||||
$formElementConfiguration,
|
||||
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||
);
|
||||
$formElementsByGroup[$formElementConfiguration['group']][] = [
|
||||
'identifier' => $formElementName,
|
||||
'label' => $formElementConfiguration['label'],
|
||||
'description' => $formElementConfiguration['description'] ?? '',
|
||||
'requestType' => 'event',
|
||||
'event' => 'typo3:form:insert-element-click',
|
||||
'sorting' => $formElementConfiguration['groupSorting'],
|
||||
'icon' => $formElementConfiguration['iconIdentifier'],
|
||||
];
|
||||
}
|
||||
$formGroups = [];
|
||||
foreach ($prototypeConfiguration['formEditor']['formElementGroups'] ?? [] as $groupName => $groupConfiguration) {
|
||||
if (!isset($formElementsByGroup[$groupName])) {
|
||||
continue;
|
||||
}
|
||||
usort($formElementsByGroup[$groupName], static function ($a, $b) {
|
||||
return $a['sorting'] - $b['sorting'];
|
||||
});
|
||||
$groupConfiguration = $this->translationService->translateValuesRecursive(
|
||||
$groupConfiguration,
|
||||
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||
);
|
||||
$formGroups[$groupName] = [
|
||||
'identifier' => $groupName,
|
||||
'items' => $formElementsByGroup[$groupName],
|
||||
'label' => $groupConfiguration['label'],
|
||||
];
|
||||
}
|
||||
return $formGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the YAML settings by the 'formEditor' keyword.
|
||||
*/
|
||||
protected function getFormEditorDefinitions(array $prototypeConfiguration): array
|
||||
{
|
||||
$formEditorDefinitions = [];
|
||||
foreach ([$prototypeConfiguration, $prototypeConfiguration['formEditor']] as $configuration) {
|
||||
foreach ($configuration as $firstLevelItemKey => $firstLevelItemValue) {
|
||||
if (!str_ends_with($firstLevelItemKey, 'Definition')) {
|
||||
continue;
|
||||
}
|
||||
$reducedKey = substr($firstLevelItemKey, 0, -10);
|
||||
foreach ($firstLevelItemValue as $formEditorDefinitionKey => $formEditorDefinitionValue) {
|
||||
if (isset($formEditorDefinitionValue['formEditor'])) {
|
||||
$formEditorDefinitionValue = array_intersect_key($formEditorDefinitionValue, array_flip(['formEditor']));
|
||||
$formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue['formEditor'];
|
||||
} else {
|
||||
$formEditorDefinitions[$reducedKey][$formEditorDefinitionKey] = $formEditorDefinitionValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$formEditorDefinitions = ArrayUtility::reIndexNumericArrayKeysRecursive($formEditorDefinitions);
|
||||
$formEditorDefinitions = $this->formEditorEnrichmentService->enrichFormEditorDefinitions($formEditorDefinitions);
|
||||
return $this->translationService->translateValuesRecursive(
|
||||
$formEditorDefinitions,
|
||||
$prototypeConfiguration['formEditor']['translationFiles'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize ModuleTemplate and register docheader icons.
|
||||
*/
|
||||
protected function initializeModuleTemplate(RequestInterface $request, string $returnUrl = ''): ModuleTemplate
|
||||
{
|
||||
$moduleTemplate = $this->moduleTemplateFactory->create($request);
|
||||
$getVars = $request->getArguments();
|
||||
if (isset($getVars['action']) && $getVars['action'] === 'index') {
|
||||
$closeUrl = $returnUrl !== '' ? $returnUrl : (string)$this->coreUriBuilder->buildUriFromRoute('web_FormFormbuilder');
|
||||
$closeButton = $this->componentFactory->createCloseButton($closeUrl)
|
||||
->setDataAttributes(['identifier' => 'closeButton'])
|
||||
->setClasses('formeditor-element-close-form-button hidden');
|
||||
$moduleTemplate->addButtonToButtonBar($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
$saveButton = $this->componentFactory->createInputButton()
|
||||
->setDataAttributes(['identifier' => 'saveButton'])
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.save_button'))
|
||||
->setName('formeditor-save-form')
|
||||
->setValue('save')
|
||||
->setClasses('formeditor-element-save-form-button hidden')
|
||||
->setIcon($this->iconFactory->getIcon('actions-document-save', IconSize::SMALL))
|
||||
->setShowLabelText(true);
|
||||
$moduleTemplate->addButtonToButtonBar($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
|
||||
$undoButton = $this->componentFactory->createInputButton()
|
||||
->setDataAttributes(['identifier' => 'undoButton'])
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.undo_button'))
|
||||
->setName('formeditor-undo-form')
|
||||
->setValue('undo')
|
||||
->setClasses('formeditor-element-undo-form-button hidden disabled')
|
||||
->setIcon($this->iconFactory->getIcon('actions-edit-undo', IconSize::SMALL));
|
||||
$moduleTemplate->addButtonToButtonBar($undoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
|
||||
$redoButton = $this->componentFactory->createInputButton()
|
||||
->setDataAttributes(['identifier' => 'redoButton'])
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formEditor.redo_button'))
|
||||
->setName('formeditor-redo-form')
|
||||
->setValue('redo')
|
||||
->setClasses('formeditor-element-redo-form-button hidden disabled')
|
||||
->setIcon($this->iconFactory->getIcon('actions-edit-redo', IconSize::SMALL));
|
||||
$moduleTemplate->addButtonToButtonBar($redoButton, ButtonBar::BUTTON_POSITION_LEFT, 5);
|
||||
}
|
||||
return $moduleTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the form editor templates.
|
||||
*/
|
||||
protected function renderFormEditorTemplates(array $prototypeConfiguration, array $formEditorDefinitions): string
|
||||
{
|
||||
$fluidConfiguration = $prototypeConfiguration['formEditor']['formEditorFluidConfiguration'] ?? null;
|
||||
$formEditorPartials = $prototypeConfiguration['formEditor']['formEditorPartials'] ?? null;
|
||||
if (!isset($fluidConfiguration['templatePathAndFilename'])) {
|
||||
throw new RenderingException('The option templatePathAndFilename must be set.', 1485636499);
|
||||
}
|
||||
if (!isset($fluidConfiguration['layoutRootPaths']) || !is_array($fluidConfiguration['layoutRootPaths'])) {
|
||||
throw new RenderingException('The option layoutRootPaths must be set.', 1480294721);
|
||||
}
|
||||
if (!isset($fluidConfiguration['partialRootPaths']) || !is_array($fluidConfiguration['partialRootPaths'])) {
|
||||
throw new RenderingException('The option partialRootPaths must be set.', 1480294722);
|
||||
}
|
||||
|
||||
$elementsCategories = $this->getInsertRenderablesPanelConfiguration($prototypeConfiguration, $formEditorDefinitions['formElements']);
|
||||
$pagesCategories = $this->getInsertRenderablesPanelConfiguration($prototypeConfiguration, $formEditorDefinitions['formElements'], true);
|
||||
$viewFactoryData = new ViewFactoryData(
|
||||
templatePathAndFilename: $fluidConfiguration['templatePathAndFilename'],
|
||||
partialRootPaths: $fluidConfiguration['partialRootPaths'],
|
||||
layoutRootPaths: $fluidConfiguration['layoutRootPaths'],
|
||||
request: $this->request,
|
||||
);
|
||||
$view = $this->viewFactory->create($viewFactoryData);
|
||||
$view->assignMultiple([
|
||||
'elementsCategoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($elementsCategories, false),
|
||||
'pagesCategoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($pagesCategories, false),
|
||||
'formEditorPartials' => $formEditorPartials,
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo move this to FormDefinitionConversionService
|
||||
*/
|
||||
protected function transformFormDefinitionForFormEditor(array $prototypeConfiguration, array $formDefinition, string $formPersistenceIdentifier): array
|
||||
{
|
||||
/** @var array<string, list<string>> $multiValueFormElementProperties */
|
||||
$multiValueFormElementProperties = [];
|
||||
/** @var array<string, list<string>> $multiValueFinisherProperties */
|
||||
$multiValueFinisherProperties = [];
|
||||
foreach ($prototypeConfiguration['formElementsDefinition'] as $type => $configuration) {
|
||||
if (!isset($configuration['formEditor']['editors'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($configuration['formEditor']['editors'] as $editorConfiguration) {
|
||||
if (($editorConfiguration['templateName'] ?? '') === 'Inspector-PropertyGridEditor') {
|
||||
$multiValueFormElementProperties[$type][] = $editorConfiguration['propertyPath'];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($prototypeConfiguration['formElementsDefinition']['Form']['formEditor']['propertyCollections']['finishers'] ?? [] as $configuration) {
|
||||
if (!isset($configuration['editors'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($configuration['editors'] as $editorConfiguration) {
|
||||
if (($editorConfiguration['templateName'] ?? '') === 'Inspector-PropertyGridEditor') {
|
||||
$multiValueFinisherProperties[$configuration['identifier']][] = $editorConfiguration['propertyPath'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$formDefinition = $this->filterEmptyArrays($formDefinition);
|
||||
$formDefinition = $this->migrateEmailFinisherRecipients($formDefinition);
|
||||
|
||||
$formDefinition = $this->transformMultiValuePropertiesForFormEditor(
|
||||
$formDefinition,
|
||||
'type',
|
||||
$multiValueFormElementProperties
|
||||
);
|
||||
$formDefinition = $this->transformMultiValuePropertiesForFormEditor(
|
||||
$formDefinition,
|
||||
'identifier',
|
||||
$multiValueFinisherProperties
|
||||
);
|
||||
|
||||
$rtePropertyPaths = $this->formDefinitionConversionService->extractRtePropertyPaths($prototypeConfiguration);
|
||||
if ($rtePropertyPaths !== []) {
|
||||
$formDefinition = $this->formDefinitionConversionService->transformRteContentForRichTextEditor(
|
||||
$formDefinition,
|
||||
$rtePropertyPaths
|
||||
);
|
||||
}
|
||||
|
||||
$formDefinition = $this->formDefinitionConversionService->sanitizeHtml($formDefinition, $rtePropertyPaths);
|
||||
$formDefinition = $this->formDefinitionConversionService->addHmacData($formDefinition, $formPersistenceIdentifier);
|
||||
return $this->formDefinitionConversionService->migrateFinisherConfiguration($formDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Some data needs a transformation before it can be used by the
|
||||
* form editor. This rules for multivalue elements like select
|
||||
* elements. To ensure the right sorting if the data goes into
|
||||
* javascript, we need to do transformations:
|
||||
*
|
||||
* [
|
||||
* '5' => '5',
|
||||
* '4' => '4',
|
||||
* '3' => '3'
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* This method transform this into:
|
||||
*
|
||||
* [
|
||||
* [
|
||||
* _label => '5'
|
||||
* _value => 5
|
||||
* ],
|
||||
* [
|
||||
* _label => '4'
|
||||
* _value => 4
|
||||
* ],
|
||||
* [
|
||||
* _label => '3'
|
||||
* _value => 3
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @param array<string, list<string>> $multiValueProperties
|
||||
*/
|
||||
protected function transformMultiValuePropertiesForFormEditor(
|
||||
array $formDefinition,
|
||||
string $identifierProperty,
|
||||
array $multiValueProperties
|
||||
): array {
|
||||
$output = $formDefinition;
|
||||
foreach ($formDefinition as $key => $value) {
|
||||
$identifier = $value[$identifierProperty] ?? null;
|
||||
if (is_string($identifier) && array_key_exists($identifier, $multiValueProperties)) {
|
||||
$multiValuePropertiesForIdentifier = $multiValueProperties[$identifier];
|
||||
foreach ($multiValuePropertiesForIdentifier as $multiValueProperty) {
|
||||
if (!ArrayUtility::isValidPath($value, $multiValueProperty, '.')) {
|
||||
continue;
|
||||
}
|
||||
$multiValuePropertyData = ArrayUtility::getValueByPath($value, $multiValueProperty, '.');
|
||||
if (!is_array($multiValuePropertyData)) {
|
||||
continue;
|
||||
}
|
||||
$newMultiValuePropertyData = [];
|
||||
foreach ($multiValuePropertyData as $k => $v) {
|
||||
$newMultiValuePropertyData[] = [
|
||||
'_label' => $v,
|
||||
'_value' => $k,
|
||||
];
|
||||
}
|
||||
$value = ArrayUtility::setValueByPath($value, $multiValueProperty, $newMultiValuePropertyData, '.');
|
||||
}
|
||||
}
|
||||
$output[$key] = $value;
|
||||
if (is_array($value)) {
|
||||
$output[$key] = $this->transformMultiValuePropertiesForFormEditor(
|
||||
$value,
|
||||
$identifierProperty,
|
||||
$multiValueProperties
|
||||
);
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keys from an array if the key value is an empty array
|
||||
*/
|
||||
protected function filterEmptyArrays(array $array): array
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if (!is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
if (empty($value)) {
|
||||
unset($array[$key]);
|
||||
continue;
|
||||
}
|
||||
$array[$key] = $this->filterEmptyArrays($value);
|
||||
if (empty($array[$key])) {
|
||||
unset($array[$key]);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate single recipient options to their list successors
|
||||
*/
|
||||
protected function migrateEmailFinisherRecipients(array $formDefinition): array
|
||||
{
|
||||
foreach ($formDefinition['finishers'] ?? [] as $i => $finisherConfiguration) {
|
||||
if (!in_array($finisherConfiguration['identifier'], ['EmailToSender', 'EmailToReceiver'], true)) {
|
||||
continue;
|
||||
}
|
||||
$recipientAddress = $finisherConfiguration['options']['recipientAddress'] ?? '';
|
||||
$recipientName = $finisherConfiguration['options']['recipientName'] ?? '';
|
||||
$carbonCopyAddress = $finisherConfiguration['options']['carbonCopyAddress'] ?? '';
|
||||
$blindCarbonCopyAddress = $finisherConfiguration['options']['blindCarbonCopyAddress'] ?? '';
|
||||
$replyToAddress = $finisherConfiguration['options']['replyToAddress'] ?? '';
|
||||
if (!empty($recipientAddress)) {
|
||||
$finisherConfiguration['options']['recipients'][$recipientAddress] = $recipientName;
|
||||
}
|
||||
if (!empty($carbonCopyAddress)) {
|
||||
$finisherConfiguration['options']['carbonCopyRecipients'][$carbonCopyAddress] = '';
|
||||
}
|
||||
if (!empty($blindCarbonCopyAddress)) {
|
||||
$finisherConfiguration['options']['blindCarbonCopyRecipients'][$blindCarbonCopyAddress] = '';
|
||||
}
|
||||
if (!empty($replyToAddress)) {
|
||||
$finisherConfiguration['options']['replyToRecipients'][$replyToAddress] = '';
|
||||
}
|
||||
unset(
|
||||
$finisherConfiguration['options']['recipientAddress'],
|
||||
$finisherConfiguration['options']['recipientName'],
|
||||
$finisherConfiguration['options']['carbonCopyAddress'],
|
||||
$finisherConfiguration['options']['blindCarbonCopyAddress'],
|
||||
$finisherConfiguration['options']['replyToAddress']
|
||||
);
|
||||
$formDefinition['finishers'][$i] = $finisherConfiguration;
|
||||
}
|
||||
return $formDefinition;
|
||||
}
|
||||
|
||||
protected function flushPageCache(string $formPersistenceIdentifier): void
|
||||
{
|
||||
$pageIdList = [];
|
||||
$referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($formPersistenceIdentifier);
|
||||
foreach ($referenceRows as $referenceRow) {
|
||||
$record = BackendUtility::getRecord($referenceRow['tablename'], $referenceRow['recuid']);
|
||||
if (!$record) {
|
||||
continue;
|
||||
}
|
||||
$pageIdList[] = $record['pid'];
|
||||
}
|
||||
|
||||
foreach (array_unique($pageIdList) as $pageId) {
|
||||
$this->cacheManager->flushCachesInGroupByTag('pages', 'pageId_' . $pageId);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface as ExtbaseConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Http\ForwardResponse;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\FinisherOptionsFlexFormOverridesConverter;
|
||||
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\FlexFormFinisherOverridesConverterDto;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||
|
||||
/**
|
||||
* The frontend controller
|
||||
*
|
||||
* Scope: frontend
|
||||
* @internal
|
||||
*/
|
||||
class FormFrontendController extends ActionController
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly ConfigurationService $configurationService,
|
||||
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||
protected readonly FlexFormTools $flexFormTools,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Take the form which should be rendered from the plugin settings
|
||||
* and overlay the formDefinition with additional data from
|
||||
* flexform and typoscript settings.
|
||||
* This method is used directly to display the first page from the
|
||||
* formDefinition because its cached.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function renderAction(): ResponseInterface
|
||||
{
|
||||
$formDefinition = [];
|
||||
if (!empty($this->settings['persistenceIdentifier'])) {
|
||||
$typoScriptSettings = $this->configurationManager->getConfiguration(ExtbaseConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||
$formDefinition = $this->formPersistenceManager->load($this->settings['persistenceIdentifier'], $typoScriptSettings, $this->request);
|
||||
$formDefinition['persistenceIdentifier'] = $this->settings['persistenceIdentifier'];
|
||||
$formDefinition = $this->overrideByFlexFormSettings($formDefinition);
|
||||
$formDefinition = ArrayUtility::setValueByPath($formDefinition, 'renderingOptions._originalIdentifier', $formDefinition['identifier'], '.');
|
||||
$formDefinition['identifier'] .= '-' . ($this->request->getAttribute('currentContentObject')?->data['uid'] ?? '');
|
||||
}
|
||||
$this->view->assign('formConfiguration', $formDefinition);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to display all pages / finishers except the
|
||||
* first page because its non cached.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function performAction(): ResponseInterface
|
||||
{
|
||||
return new ForwardResponse('render');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the formDefinition with additional data from the Flexform
|
||||
* settings. For now, only finisher settings are overridable.
|
||||
*/
|
||||
protected function overrideByFlexFormSettings(array $formDefinition): array
|
||||
{
|
||||
$flexFormData = $this->request->getAttribute('currentContentObject')?->data['pi_flexform'] ?? [];
|
||||
if (is_string($flexFormData) && $flexFormData !== '') {
|
||||
$flexFormData = GeneralUtility::xml2array($flexFormData);
|
||||
}
|
||||
if (!is_array($flexFormData) || $flexFormData === []) {
|
||||
return $formDefinition;
|
||||
}
|
||||
if (isset($formDefinition['finishers'])) {
|
||||
$prototypeName = $formDefinition['prototypeName'] ?? 'standard';
|
||||
$prototypeConfiguration = $this->configurationService->getPrototypeConfiguration($prototypeName);
|
||||
foreach ($formDefinition['finishers'] as $index => $formFinisherDefinition) {
|
||||
$finisherIdentifier = $formFinisherDefinition['identifier'];
|
||||
$sheetIdentifier = $this->getFlexformSheetIdentifier($formDefinition, $prototypeName, $finisherIdentifier);
|
||||
$flexFormSheetSettings = $this->getFlexFormSettingsFromSheet($flexFormData, $sheetIdentifier);
|
||||
if (($this->settings['overrideFinishers'] ?? false) && isset($flexFormSheetSettings['finishers'][$finisherIdentifier])) {
|
||||
$prototypeFinisherDefinition = $prototypeConfiguration['finishersDefinition'][$finisherIdentifier] ?? [];
|
||||
$converterDto = GeneralUtility::makeInstance(
|
||||
FlexFormFinisherOverridesConverterDto::class,
|
||||
$prototypeFinisherDefinition,
|
||||
$formFinisherDefinition,
|
||||
$finisherIdentifier,
|
||||
$flexFormSheetSettings
|
||||
);
|
||||
// Iterate over all `prototypes.<prototypeName>.finishersDefinition.<finisherIdentifier>.FormEngine.elements` values
|
||||
GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeFinisherDefinition['FormEngine']['elements'])->forEach(
|
||||
GeneralUtility::makeInstance(
|
||||
ArrayProcessing::class,
|
||||
'modifyFinisherOptionsFromFlexFormOverrides',
|
||||
'^(.*)(?:\.config\.type|\.section)$',
|
||||
GeneralUtility::makeInstance(FinisherOptionsFlexFormOverridesConverter::class, $converterDto)
|
||||
)
|
||||
);
|
||||
$formDefinition['finishers'][$index] = $converterDto->getFinisherDefinition();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $formDefinition;
|
||||
}
|
||||
|
||||
protected function getFlexformSheetIdentifier(array $formDefinition, string $prototypeName, string $finisherIdentifier): string
|
||||
{
|
||||
return md5(
|
||||
implode('', [
|
||||
$formDefinition['persistenceIdentifier'],
|
||||
$prototypeName,
|
||||
$formDefinition['identifier'],
|
||||
$finisherIdentifier,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
protected function getFlexFormSettingsFromSheet(array $flexForm, string $sheetIdentifier): array
|
||||
{
|
||||
$sheetData = [];
|
||||
$sheetData['data'] = array_filter(
|
||||
$flexForm['data'] ?? [],
|
||||
static function ($key) use ($sheetIdentifier) {
|
||||
return $key === $sheetIdentifier;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
if (empty($sheetData['data'])) {
|
||||
return [];
|
||||
}
|
||||
$sheetDataXml = $this->flexFormTools->flexArray2Xml($sheetData);
|
||||
return $this->flexFormTools->convertFlexFormContentToArray($sheetDataXml)['settings'] ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Charset\CharsetConverter;
|
||||
use TYPO3\CMS\Core\Http\AllowedMethodsTrait;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Pagination\ArrayPaginator;
|
||||
use TYPO3\CMS\Core\Pagination\SimplePagination;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
|
||||
use TYPO3\CMS\Form\Domain\DTO\SearchCriteria;
|
||||
use TYPO3\CMS\Form\Domain\Repository\FormDefinitionRepository;
|
||||
use TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent;
|
||||
use TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent;
|
||||
use TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent;
|
||||
use TYPO3\CMS\Form\Exception as FormException;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface as ExtFormConfigurationManagerInterface;
|
||||
use TYPO3\CMS\Form\Mvc\Configuration\YamlSource;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
||||
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
|
||||
use TYPO3\CMS\Form\Service\DatabaseService;
|
||||
use TYPO3\CMS\Form\Service\TranslationService;
|
||||
|
||||
/**
|
||||
* The form manager controller
|
||||
*
|
||||
* Scope: backend
|
||||
* @internal
|
||||
*/
|
||||
class FormManagerController extends ActionController
|
||||
{
|
||||
use AllowedMethodsTrait;
|
||||
|
||||
protected const JS_MODULE_NAMES = ['app', 'viewModel'];
|
||||
protected const PAGINATION_MAX = 20;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected readonly PageRenderer $pageRenderer,
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly DatabaseService $databaseService,
|
||||
protected readonly FormPersistenceManagerInterface $formPersistenceManager,
|
||||
protected readonly ExtFormConfigurationManagerInterface $extFormConfigurationManager,
|
||||
protected readonly TranslationService $translationService,
|
||||
protected readonly CharsetConverter $charsetConverter,
|
||||
protected readonly UriBuilder $coreUriBuilder,
|
||||
protected readonly YamlSource $yamlSource,
|
||||
protected readonly ComponentFactory $componentFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display the Form Manager. The main showing available forms.
|
||||
*/
|
||||
protected function indexAction(int $page = 1, string $searchTerm = '', string $orderField = '', ?string $orderDirection = null): ResponseInterface
|
||||
{
|
||||
$formSettings = $this->getFormSettings();
|
||||
$hasForms = $this->formPersistenceManager->hasForms([]);
|
||||
$searchCriteria = new SearchCriteria(searchTerm: trim($searchTerm), orderField: $orderField, orderDirection: $orderDirection);
|
||||
$returnUrl = $this->request->getAttribute('normalizedParams')->getRequestUri();
|
||||
$forms = $hasForms ? $this->getAvailableFormDefinitions($formSettings, $searchCriteria, $returnUrl) : [];
|
||||
$arrayPaginator = new ArrayPaginator($forms, $page, self::PAGINATION_MAX);
|
||||
$pagination = new SimplePagination($arrayPaginator);
|
||||
$moduleTemplate = $this->initializeModuleTemplate($this->request, $page, $searchTerm);
|
||||
$moduleTemplate->assignMultiple([
|
||||
'paginator' => $arrayPaginator,
|
||||
'pagination' => $pagination,
|
||||
'searchTerm' => $searchTerm,
|
||||
'orderField' => $searchCriteria->orderField,
|
||||
'orderDirection' => $searchCriteria->orderDirection,
|
||||
'hasForms' => $hasForms,
|
||||
'stylesheets' => $formSettings['formManager']['stylesheets'],
|
||||
'formManagerAppInitialData' => json_encode($this->getFormManagerAppInitialData($formSettings)),
|
||||
]);
|
||||
$javaScriptModules = array_map(
|
||||
static fn(string $name) => JavaScriptModuleInstruction::create($name),
|
||||
array_filter(
|
||||
$formSettings['formManager']['dynamicJavaScriptModules'] ?? [],
|
||||
fn(string $name) => in_array($name, self::JS_MODULE_NAMES, true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
)
|
||||
);
|
||||
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||
JavaScriptModuleInstruction::create('@typo3/form/backend/helper.js', 'Helper')
|
||||
->invoke('dispatchFormManager', $javaScriptModules, $this->getFormManagerAppInitialData($formSettings))
|
||||
);
|
||||
array_map($this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(...), $javaScriptModules);
|
||||
$moduleTemplate->setModuleClass($this->request->getPluginName() . '_' . $this->request->getControllerName());
|
||||
$moduleTemplate->setFlashMessageQueue($this->getFlashMessageQueue());
|
||||
$moduleTemplate->setTitle(
|
||||
$this->getLanguageService()->translate('title', 'form.module')
|
||||
);
|
||||
return $moduleTemplate->renderResponse('Backend/FormManager/Index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the "create" action.
|
||||
* This action uses the Fluid JsonView::class as view.
|
||||
*/
|
||||
protected function initializeCreateAction(): void
|
||||
{
|
||||
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||
$this->defaultViewObjectName = JsonView::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Form and redirects to the Form Editor
|
||||
*
|
||||
* @throws FormException
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
protected function createAction(string $formName, string $templatePath, string $prototypeName, string $storage, string $storageLocation): ResponseInterface
|
||||
{
|
||||
$formSettings = $this->getFormSettings();
|
||||
if (!$this->formPersistenceManager->isAllowedStorageLocation($storageLocation)) {
|
||||
throw new PersistenceManagerException(sprintf('Save to storage location "%s" is not allowed', $storageLocation), 1614500657);
|
||||
}
|
||||
if (!$this->isValidTemplatePath($formSettings, $prototypeName, $templatePath)) {
|
||||
throw new FormException(sprintf('The template path "%s" is not allowed', $templatePath), 1329233410);
|
||||
}
|
||||
if (empty($formName)) {
|
||||
throw new FormException('No form name', 1472312204);
|
||||
}
|
||||
$templatePath = GeneralUtility::getFileAbsFileName($templatePath);
|
||||
$form = $this->yamlSource->load([$templatePath]);
|
||||
$form['label'] = $formName;
|
||||
$form['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
|
||||
$form['prototypeName'] = $prototypeName;
|
||||
$formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($storage, $form['identifier'], $storageLocation);
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeFormIsCreatedEvent($formPersistenceIdentifier, $form, $this->request)
|
||||
);
|
||||
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||
$form = $event->form;
|
||||
$form = ArrayUtility::stripTagsFromValuesRecursive($form);
|
||||
try {
|
||||
$formIdentifier = $this->formPersistenceManager->save($formPersistenceIdentifier, $form, [], $storageLocation);
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'url' => (string)$this->coreUriBuilder->buildUriFromRoute('form_editor', ['formPersistenceIdentifier' => $formIdentifier->identifier]),
|
||||
];
|
||||
} catch (PersistenceManagerException $e) {
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
// createAction uses the Extbase JsonView::class.
|
||||
// That's why we have to set the view variables in this way.
|
||||
/** @var JsonView $view */
|
||||
$view = $this->view;
|
||||
$view->assign('response', $response);
|
||||
$view->setVariablesToRender([
|
||||
'response',
|
||||
]);
|
||||
return $this->jsonResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the duplicate action.
|
||||
* This action uses the Fluid JsonView::class as view.
|
||||
*/
|
||||
protected function initializeDuplicateAction(): void
|
||||
{
|
||||
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||
$this->defaultViewObjectName = JsonView::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates a given formDefinition and redirects to the Form Editor
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
protected function duplicateAction(string $formName, string $formPersistenceIdentifier, string $storage, string $storageLocation): ResponseInterface
|
||||
{
|
||||
if (!$this->formPersistenceManager->isAllowedStorageLocation($storageLocation)) {
|
||||
throw new PersistenceManagerException(sprintf('Save to storage location "%s" is not allowed', $storageLocation), 1614500658);
|
||||
}
|
||||
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Read of "%s" is not allowed', $formPersistenceIdentifier), 1614500659);
|
||||
}
|
||||
$formToDuplicate = $this->formPersistenceManager->load($formPersistenceIdentifier);
|
||||
$formToDuplicate['label'] = $formName;
|
||||
$formToDuplicate['identifier'] = $this->formPersistenceManager->getUniqueIdentifier($this->convertFormNameToIdentifier($formName));
|
||||
$formPersistenceIdentifier = $this->formPersistenceManager->getUniquePersistenceIdentifier($storage, $formToDuplicate['identifier'], $storageLocation);
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeFormIsDuplicatedEvent($formPersistenceIdentifier, $formToDuplicate, $this->request)
|
||||
);
|
||||
$formPersistenceIdentifier = $event->formPersistenceIdentifier;
|
||||
$formToDuplicate = $event->form;
|
||||
$formToDuplicate = ArrayUtility::stripTagsFromValuesRecursive($formToDuplicate);
|
||||
try {
|
||||
$formIdentifier = $this->formPersistenceManager->save($formPersistenceIdentifier, $formToDuplicate, [], $storageLocation);
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'url' => (string)$this->coreUriBuilder->buildUriFromRoute('form_editor', ['formPersistenceIdentifier' => $formIdentifier->identifier]),
|
||||
];
|
||||
} catch (PersistenceManagerException $e) {
|
||||
$response = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
// createAction uses the Extbase JsonView::class.
|
||||
// That's why we have to set the view variables in this way.
|
||||
/** @var JsonView $view */
|
||||
$view = $this->view;
|
||||
$view->assign('response', $response);
|
||||
$view->setVariablesToRender([
|
||||
'response',
|
||||
]);
|
||||
return $this->jsonResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the references action.
|
||||
* This action uses the Fluid JsonView::class as view.
|
||||
*/
|
||||
protected function initializeReferencesAction(): void
|
||||
{
|
||||
$this->defaultViewObjectName = JsonView::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show references to this persistence identifier
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
protected function referencesAction(string $formPersistenceIdentifier): ResponseInterface
|
||||
{
|
||||
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Access to "%s" is not allowed', $formPersistenceIdentifier), 1614500661);
|
||||
}
|
||||
// referencesAction uses the extbase JsonView::class.
|
||||
// That's why we have to set the view variables in this way.
|
||||
/** @var JsonView $view */
|
||||
$view = $this->view;
|
||||
$view->assign('references', $this->getProcessedReferencesRows($formPersistenceIdentifier));
|
||||
$view->assign('formPersistenceIdentifier', $formPersistenceIdentifier);
|
||||
$view->setVariablesToRender([
|
||||
'references',
|
||||
'formPersistenceIdentifier',
|
||||
]);
|
||||
return $this->jsonResponse();
|
||||
}
|
||||
|
||||
protected function initializeDeleteAction(): void
|
||||
{
|
||||
$this->assertAllowedHttpMethod($this->request, 'POST');
|
||||
$this->defaultViewObjectName = JsonView::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a formDefinition identified by the $formPersistenceIdentifier.
|
||||
*
|
||||
* @throws PersistenceManagerException
|
||||
*/
|
||||
protected function deleteAction(string $formPersistenceIdentifier): ResponseInterface
|
||||
{
|
||||
$formSettings = $this->getFormSettings();
|
||||
if (!$this->formPersistenceManager->isAllowedPersistenceIdentifier($formPersistenceIdentifier)) {
|
||||
throw new PersistenceManagerException(sprintf('Delete "%s" is not allowed', $formPersistenceIdentifier), 1768562524);
|
||||
}
|
||||
|
||||
$hasReferences = !empty($this->databaseService->getReferencesByPersistenceIdentifier($formPersistenceIdentifier));
|
||||
|
||||
if ($hasReferences) {
|
||||
$response = $this->getErrorResponseForDeleteAction($formSettings, $formPersistenceIdentifier);
|
||||
} else {
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeFormIsDeletedEvent($formPersistenceIdentifier, $this->request)
|
||||
);
|
||||
if ($event->preventDeletion) {
|
||||
$response = $this->getErrorResponseForDeleteAction($formSettings, $formPersistenceIdentifier);
|
||||
} else {
|
||||
$this->formPersistenceManager->delete($formPersistenceIdentifier, []);
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'url' => $this->uriBuilder->uriFor('index', [], 'FormManager'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// deleteAction uses the extbase JsonView::class.
|
||||
// That's why we have to set the view variables in this way.
|
||||
/** @var JsonView $view */
|
||||
$view = $this->view;
|
||||
$view->assign('response', $response);
|
||||
$view->setVariablesToRender([
|
||||
'response',
|
||||
]);
|
||||
return $this->jsonResponse();
|
||||
}
|
||||
|
||||
protected function getErrorResponseForDeleteAction(array $formSettings, string $formPersistenceIdentifier): array
|
||||
{
|
||||
$controllerConfiguration = $this->translationService->translateValuesRecursive(
|
||||
$formSettings['formManager']['controller'],
|
||||
$formSettings['formManager']['translationFiles'] ?? []
|
||||
);
|
||||
return [
|
||||
'status' => 'error',
|
||||
'title' => $controllerConfiguration['deleteAction']['errorTitle'],
|
||||
'message' => sprintf($controllerConfiguration['deleteAction']['errorMessage'], $formPersistenceIdentifier),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormSettings(): array
|
||||
{
|
||||
$typoScriptSettings = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'form');
|
||||
$formSettings = $this->extFormConfigurationManager->getYamlConfiguration($typoScriptSettings, false);
|
||||
if (!isset($formSettings['formManager'])) {
|
||||
// Config sub array formManager is crucial and should always exist. If it does
|
||||
// not, this indicates an issue in config loading logic. Except in this case.
|
||||
throw new \LogicException('Configuration could not be loaded', 1723717461);
|
||||
}
|
||||
return $formSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the json encoded data which is used by the form editor
|
||||
* JavaScript app.
|
||||
*/
|
||||
protected function getFormManagerAppInitialData(array $formSettings): array
|
||||
{
|
||||
$formManagerAppInitialData = [
|
||||
'selectablePrototypesConfiguration' => $formSettings['formManager']['selectablePrototypesConfiguration'],
|
||||
'endpoints' => [
|
||||
'create' => $this->uriBuilder->uriFor('create'),
|
||||
'duplicate' => $this->uriBuilder->uriFor('duplicate'),
|
||||
'delete' => $this->uriBuilder->uriFor('delete'),
|
||||
'references' => $this->uriBuilder->uriFor('references'),
|
||||
],
|
||||
'accessibleStorageAdapters' => $this->formPersistenceManager->getAccessibleStorageAdapters(),
|
||||
];
|
||||
$formManagerAppInitialData = ArrayUtility::reIndexNumericArrayKeysRecursive($formManagerAppInitialData);
|
||||
return $this->translationService->translateValuesRecursive(
|
||||
$formManagerAppInitialData,
|
||||
$formSettings['formManager']['translationFiles'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all formDefinitions which can be loaded through form persistence
|
||||
* manager. Enrich this data by a reference counter.
|
||||
*/
|
||||
protected function getAvailableFormDefinitions(array $formSettings, SearchCriteria $searchCriteria, string $returnUrl = ''): array
|
||||
{
|
||||
$availableFormDefinitions = [];
|
||||
|
||||
foreach ($this->formPersistenceManager->listForms($formSettings, $searchCriteria) as $formMetadata) {
|
||||
|
||||
if ($formMetadata->persistenceIdentifier && !$formMetadata->invalid && !$formMetadata->readOnly) {
|
||||
$editUrl = (string)$this->coreUriBuilder->buildUriFromRoute(
|
||||
'form_editor',
|
||||
array_filter([
|
||||
'formPersistenceIdentifier' => $formMetadata->persistenceIdentifier,
|
||||
'returnUrl' => $returnUrl,
|
||||
])
|
||||
);
|
||||
$formMetadata = $formMetadata->withEditUrl($editUrl);
|
||||
}
|
||||
|
||||
$actions = $this->getRecordActions($formMetadata->persistenceIdentifier);
|
||||
$formMetadata = $formMetadata->withActions($actions);
|
||||
|
||||
if ($searchCriteria->searchTerm === ''
|
||||
|| $this->valueContainsSearchTerm($formMetadata->name, $searchCriteria->searchTerm)
|
||||
|| ($formMetadata->persistenceIdentifier && $this->valueContainsSearchTerm($formMetadata->persistenceIdentifier, $searchCriteria->searchTerm))
|
||||
) {
|
||||
$availableFormDefinitions[] = $formMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return $availableFormDefinitions;
|
||||
}
|
||||
|
||||
protected function valueContainsSearchTerm(string $value, string $searchTerm): bool
|
||||
{
|
||||
return str_contains(strtolower($value), strtolower($searchTerm));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with information about the references for a
|
||||
* formDefinition identified by $persistenceIdentifier.
|
||||
*/
|
||||
protected function getProcessedReferencesRows(string $persistenceIdentifier): array
|
||||
{
|
||||
if (empty($persistenceIdentifier)) {
|
||||
throw new \InvalidArgumentException('$persistenceIdentifier must not be empty.', 1477071939);
|
||||
}
|
||||
$references = [];
|
||||
$referenceRows = $this->databaseService->getReferencesByPersistenceIdentifier($persistenceIdentifier);
|
||||
foreach ($referenceRows as $referenceRow) {
|
||||
$record = $this->getRecord($referenceRow['tablename'], $referenceRow['recuid']);
|
||||
if (!$record) {
|
||||
continue;
|
||||
}
|
||||
$pageRecord = $this->getRecord('pages', $record['pid']);
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$referenceRow['tablename'] => [
|
||||
$referenceRow['recuid'] => 'edit',
|
||||
],
|
||||
],
|
||||
'module' => 'web_FormFormbuilder',
|
||||
'returnUrl' => $this->getModuleUrl('web_FormFormbuilder'),
|
||||
];
|
||||
$references[] = [
|
||||
'recordPageTitle' => is_array($pageRecord) ? BackendUtility::getRecordTitle('pages', $pageRecord) : '',
|
||||
'recordTitle' => BackendUtility::getRecordTitle($referenceRow['tablename'], $record),
|
||||
'recordIcon' => $this->iconFactory->getIconForRecord($referenceRow['tablename'], $record, IconSize::SMALL)->render(),
|
||||
'recordUid' => $referenceRow['recuid'],
|
||||
'recordEditUrl' => $this->getModuleUrl('record_edit', $urlParameters),
|
||||
];
|
||||
}
|
||||
return $references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given $templatePath for a given $prototypeName is valid
|
||||
* and accessible.
|
||||
*
|
||||
* Valid template paths has to be configured within
|
||||
* formManager.selectablePrototypesConfiguration.[('identifier': $prototypeName)].newFormTemplates.[('templatePath': $templatePath)]
|
||||
*/
|
||||
protected function isValidTemplatePath(array $formSettings, string $prototypeName, string $templatePath): bool
|
||||
{
|
||||
$isValid = false;
|
||||
foreach ($formSettings['formManager']['selectablePrototypesConfiguration'] as $prototypesConfiguration) {
|
||||
if ($prototypesConfiguration['identifier'] !== $prototypeName) {
|
||||
continue;
|
||||
}
|
||||
foreach ($prototypesConfiguration['newFormTemplates'] as $templatesConfiguration) {
|
||||
if ($templatesConfiguration['templatePath'] !== $templatePath) {
|
||||
continue;
|
||||
}
|
||||
$isValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$templatePath = GeneralUtility::getFileAbsFileName($templatePath);
|
||||
if (!is_file($templatePath)) {
|
||||
$isValid = false;
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record actions
|
||||
*
|
||||
* @return array
|
||||
* @throws RouteNotFoundException
|
||||
*/
|
||||
protected function getRecordActions(string $persistenceIdentifier): array
|
||||
{
|
||||
if (!MathUtility::canBeInterpretedAsInteger($persistenceIdentifier)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
|
||||
// History button
|
||||
$urlParameters = [
|
||||
'element' => FormDefinitionRepository::TABLE_NAME . ':' . $persistenceIdentifier,
|
||||
'returnUrl' => $this->request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$actions['recordHistoryUrl'] = (string)$this->coreUriBuilder->buildUriFromRoute('record_history', $urlParameters);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init ModuleTemplate and register document header buttons
|
||||
*/
|
||||
protected function initializeModuleTemplate(ServerRequestInterface $request, int $page, string $searchTerm): ModuleTemplate
|
||||
{
|
||||
$moduleTemplate = $this->moduleTemplateFactory->create($request);
|
||||
// Create new
|
||||
$addFormButton = $this->componentFactory->createLinkButton()
|
||||
->setDataAttributes(['identifier' => 'newForm'])
|
||||
->setHref('#')
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:formManager.create_new_form'))
|
||||
->setShowLabelText(true)
|
||||
->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL));
|
||||
$moduleTemplate->addButtonToButtonBar($addFormButton);
|
||||
// Shortcut
|
||||
$arguments = [];
|
||||
if ($searchTerm) {
|
||||
$arguments['tx_form_web_formformbuilder']['searchTerm'] = $searchTerm;
|
||||
$arguments['tx_form_web_formformbuilder']['controller'] = 'FormManager';
|
||||
}
|
||||
if ($page > 1) {
|
||||
$arguments['tx_form_web_formformbuilder']['page'] = $page;
|
||||
$arguments['tx_form_web_formformbuilder']['controller'] = 'FormManager';
|
||||
}
|
||||
$moduleTemplate->getDocHeaderComponent()->setShortcutContext(
|
||||
'web_FormFormbuilder',
|
||||
$this->getLanguageService()->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:module.shortcut_name'),
|
||||
$arguments
|
||||
);
|
||||
return $moduleTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a form identifier which is the lower cased form name.
|
||||
*/
|
||||
protected function convertFormNameToIdentifier(string $formName): string
|
||||
{
|
||||
$formName = \Normalizer::normalize($formName) ?: $formName;
|
||||
$formIdentifier = $this->charsetConverter->utf8_char_mapping($formName);
|
||||
$formIdentifier = (string)preg_replace('/[^a-zA-Z0-9-_]/', '', $formIdentifier);
|
||||
return lcfirst($formIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper used for unit testing.
|
||||
*/
|
||||
protected function getRecord(string $table, int $uid): ?array
|
||||
{
|
||||
return BackendUtility::getRecord($table, $uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper used for unit testing.
|
||||
*/
|
||||
protected function getModuleUrl(string $moduleName, array $urlParameters = []): string
|
||||
{
|
||||
return (string)$this->coreUriBuilder->buildUriFromRoute($moduleName, $urlParameters);
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user