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']; } }