Commit 553681a2 authored by Ralf Zimmermann's avatar Ralf Zimmermann Committed by Christian Kuhn
Browse files

[FEATURE] EXT:form - integrate new form framework

The main purpose of this patch is to integrate a flexible framework for
building forms. It replaces the legacy 'form wizard' based on ExtJS and
the depending frontend rendering system.

The new backend 'form editor' relies on vanilla JS and jQuery.
Different JS patterns have been applied to ensure a modern architecture,
high flexibility and extensibility.

A new backend module lists all existing forms and allows the creation
of new ones. The 'mailform' content element is reworked. It lists
available forms and enables the backend editor to override certain
settings, e.g. 'finisher' settings (formerly known as 'postProcessors').

Till now it was not possible to customize and extend the 'form editor'.
To allow the registration of new finishers, validators and
pre-defined form elements a lot of architectural changes were needed.
After a long conceptional phase the team decided to remove the former
code base, backport the 'form' package of the Flow project and improve
the given concepts. The result is a new form extension. A lot of code
received major improvements and tons of additional features have been
integrated.

The list of features is long and impressive. The documentation - which
is part of a future patch - will explain the ideas, concept and
architecture as well as the functionality in detail.

This patch marks the beginning of a series of patches. Further work is
needed to implement a better UI and more tests. The currently integrated
element tree cannot be finished for now. We plan to use the new TYPO3
SVG tree but have to wait for the drag and drop implementation.
Furthermore, the old form wizard will be moved to a separate extension
for backward compatibility.

Resolves: #77910
Releases: master
Change-Id: Idde8453bc573da835959fa3e51e30f57792d98b0
Reviewed-on: https://review.typo3.org/50311


Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: Björn Jacob's avatarBjoern Jacob <bjoern.jacob@tritum.de>
Tested-by: Björn Jacob's avatarBjoern Jacob <bjoern.jacob@tritum.de>
Reviewed-by: default avatarRalf Zimmermann <ralf.zimmermann@tritum.de>
Tested-by: default avatarRalf Zimmermann <ralf.zimmermann@tritum.de>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Reviewed-by: Alexander Opitz's avatarAlexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Alexander Opitz's avatarAlexander Opitz <opitz.alexander@googlemail.com>
Reviewed-by: Andreas Häfner's avatarAndreas Häfner <andreas.haefner@tritum.de>
Tested-by: Andreas Häfner's avatarAndreas Häfner <andreas.haefner@tritum.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 77377a76
This diff is collapsed.
......@@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "90479fd517e730794243c516ef4ba9ae",
"content-hash": "9ac01b16aca6a55472ecf320b6dacf31",
"hash": "9cc1058ed137d0775741d7c5701e77a1",
"content-hash": "2749660dc0a03d1b473808cf28c9cf10",
"packages": [
{
"name": "cogpowered/finediff",
......@@ -1119,6 +1119,55 @@
],
"time": "2016-05-18 14:26:46"
},
{
"name": "symfony/yaml",
"version": "v3.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2016-10-24 18:41:13"
},
{
"name": "typo3/class-alias-loader",
"version": "1.0.0",
......@@ -3234,55 +3283,6 @@
"homepage": "https://symfony.com",
"time": "2016-06-29 05:41:56"
},
{
"name": "symfony/yaml",
"version": "v3.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2016-10-24 18:41:13"
},
{
"name": "webmozart/assert",
"version": "1.1.0",
......
.. include:: ../../Includes.txt
=========================================================
Feature: #77910 - EXT:form - introduce new form framework
=========================================================
See :issue:`77910`
Description
===========
A flexible framework for building forms is integrated. It replaces the legacy 'form wizard' based on ExtJS and the
depending frontend rendering system.
The new backend 'form editor' relies on vanilla JS and jQuery. Different JS patterns have been applied to ensure
a modern architecture, high flexibility and extensibility.
A new backend module lists all existing forms and allows the creation of new ones. The 'mailform' content element
is reworked. It lists available forms and enables the backend editor to override certain settings, e.g. 'finisher'
settings (formerly known as 'postProcessors').
Till now it was not possible to customize and extend the 'form editor'. To allow the registration of new
finishers, validators and pre-defined form elements a lot of architectural changes were needed. After a long
conceptional phase the team decided to remove the former code base, backport the 'form' package of the Flow
project and improve the given ideas and concepts. The result is a new form extension. A lot of code received
major improvements and tons of additional features have been integrated.
The list of features is long and impressive. The documentation will explain the ideas, concept and architecture
as well as the functionality in detail. The following list names some of them:
* YAML as configuration and description language including inheritances and overrides.
* File based configuration.
* All JavaScript components of the form wizard (and the wizard itself) can be replaced or extended.
* Own PHP renderer for form and/ or form elements possible.
* Create entire forms via API.
* Create conditions for form elements and validators programmatically.
* Create 'prototypes' and use them as boilerplate.
* Create new form elements and use them in the wizard.
* Uploads are handled as FAL objects.
* Ships bunch of built-in finishers, like email, redirect, save to database.
* Create own finishers. Override thos in the content element.
* Create and apply own validators.
* Multi language support.
* Multi step support.
* Multiple forms per page.
* Built-in spam protection (honeypot).
Impact
======
Happy little wizard.
.. index:: Frontend, PHP-API, JavaScript, ext:form
\ No newline at end of file
<?php
namespace TYPO3\CMS\Form\ContentObject;
/*
* 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!
*/
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Model\Configuration;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* FORM cObject, a wrapper to allow to use 10 = FORM in TypoScript
* which actually executes the Extbase plugin (marked as non-cached)
*/
class FormContentObject extends AbstractContentObject
{
/**
* Renders the application defined cObject FORM
*
* The Extbase plugin "Form" is initialized. At this time, the
* controller "Frontend" action "show" does the rest.
*
* @param array $conf TS configuration for this cObject
* @return string HTML output
* @throws \InvalidArgumentException
*/
public function render($conf = [])
{
$mergedTypoScript = null;
// If the FORM configuration is retrieved from the database
// all TypoScript interpretation will be disabled for security.
if ($this->cObj->data['CType'] === 'mailform') {
// If the FORM configuration is retrieved from the database
// and a predefined form is selected then the TypoScript
// interpretation is allowed.
$renderPredefinedForm = false;
$predefinedFormIdentifier = null;
if (!empty($this->cObj->data['tx_form_predefinedform'])) {
$predefinedFormIdentifier = $this->cObj->data['tx_form_predefinedform'];
if (isset($this->getTypoScriptFrontendController()->tmpl->setup['plugin.']['tx_form.']['predefinedForms.'][$predefinedFormIdentifier . '.'])) {
$renderPredefinedForm = true;
} else {
throw new \InvalidArgumentException('No FORM configuration for identifier "' . $predefinedFormIdentifier . '" available.', 1466769483);
}
}
if ($renderPredefinedForm && $predefinedFormIdentifier) {
$mergedTypoScript = $this->getTypoScriptFrontendController()->tmpl->setup['plugin.']['tx_form.']['predefinedForms.'][$predefinedFormIdentifier . '.'];
ArrayUtility::mergeRecursiveWithOverrule($mergedTypoScript, $conf);
} else {
$bodytext = $this->cObj->data['bodytext'];
/** @var $typoScriptParser TypoScriptParser */
$typoScriptParser = GeneralUtility::makeInstance(TypoScriptParser::class);
$typoScriptParser->parse($bodytext);
$mergedTypoScript = (array)$typoScriptParser->setup;
ArrayUtility::mergeRecursiveWithOverrule($mergedTypoScript, $conf);
// Disables TypoScript interpretation since TypoScript is handled that could contain insecure settings:
$mergedTypoScript[Configuration::DISABLE_CONTENT_ELEMENT_RENDERING] = true;
}
}
// make sure the extbase plugin is marked as Uncached
$content = $this->prepareNonCacheableUserFunction(is_array($mergedTypoScript) ? $mergedTypoScript : $conf);
// Only apply stdWrap to TypoScript that was NOT created by the wizard:
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
/**
* Set up the extbase plugin to be a non-cacheable user function
*
* @param array $typoScript
* @return string the content as placeholder for USER_INT code
*/
protected function prepareNonCacheableUserFunction($typoScript)
{
$configuration = [
'userFunc' => 'TYPO3\\CMS\\Extbase\\Core\\Bootstrap->run',
'pluginName' => 'Form',
'extensionName' => 'Form',
'vendorName' => 'TYPO3\\CMS',
'controller' => 'Frontend',
'action' => 'show',
'settings' => ['typoscript' => $typoScript],
'persistence' => [],
'view' => [],
];
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT);
$substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
$content = '<!--' . $substKey . '-->';
$this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
'conf' => $configuration,
'cObj' => serialize($this->cObj),
'type' => 'FUNC'
];
$this->cObj->setUserObjectType(false);
return $content;
}
/**
* @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
*/
protected function getTypoScriptFrontendController()
{
return $GLOBALS['TSFE'];
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Form\Controller;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
/**
* The abstract form backend controller
*
* Scope: backend
*/
abstract class AbstractBackendController extends ActionController
{
/**
* @var array
*/
protected $formSettings;
/**
* @var \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface
*/
protected $formPersistenceManager;
/**
* @param \TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface $formPersistenceManager
* @return void
* @internal
*/
public function injectFormPersistenceManager(\TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface $formPersistenceManager)
{
$this->formPersistenceManager = $formPersistenceManager;
}
/**
* @internal
*/
public function initializeObject()
{
$this->formSettings = $this->objectManager->get(ConfigurationManagerInterface::class)
->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
}
/**
* Convert arrays with EXT: resource paths to web paths
*
* Input:
* [
* 100 => 'EXT:form/Resources/Public/Css/form.css'
* ]
*
* Output:
*
* [
* 0 => 'typo3/sysext/form/Resources/Public/Css/form.css'
* ]
*
* @param array $resourcePaths
* @return array
*/
protected function resolveResourcePaths(array $resourcePaths): array
{
$return = [];
foreach ($resourcePaths as $resourcePath) {
$fullResourcePath = GeneralUtility::getFileAbsFileName($resourcePath);
$resourcePath = PathUtility::getAbsoluteWebPath($fullResourcePath);
if (empty($resourcePath)) {
continue;
}
$return[] = $resourcePath;
}
return $return;
}
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Form\Controller;
/*
* 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!
*/
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendTemplateView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Utility\ArrayUtility as CoreArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Form\Utility\ArrayUtility;
use TYPO3\CMS\Lang\LanguageService;
/**
* The form editor controller
*
* Scope: backend
*/
class FormEditorController extends AbstractBackendController
{
/**
* Default View Container
*
* @var BackendTemplateView
*/
protected $defaultViewObjectName = BackendTemplateView::class;
/**
* @var array
*/
protected $prototypeConfiguration;
/**
* Displays the form editor
*
* @param string $formPersistenceIdentifier
* @param string $prototypeName
* @return void
* @throws PersistenceManagerException
* @internal
*/
public function indexAction(string $formPersistenceIdentifier, string $prototypeName = null)
{
$this->registerDocheaderButtons();
$this->view->getModuleTemplate()->setModuleName($this->request->getPluginName() . '_' . $this->request->getControllerName());
$this->view->getModuleTemplate()->setFlashMessageQueue($this->controllerContext->getFlashMessageQueue());
if (
strpos($formPersistenceIdentifier, 'EXT:') === 0
&& !$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']
) {
throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
}
$formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
$formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
if (empty($prototypeName)) {
$prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
}
$formDefinition['prototypeName'] = $prototypeName;
$configurationService = $this->objectManager->get(ConfigurationService::class);
$this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
$formEditorDefinitions = $this->getFormEditorDefinitions();
$formEditorAppInitialData = [
'formEditorDefinitions' => $formEditorDefinitions,
'formDefinition' => $formDefinition,
'formPersistenceIdentifier' => $formPersistenceIdentifier,
'prototypeName' => $prototypeName,
'endpoints' => [
'formPageRenderer' => $this->controllerContext->getUriBuilder()->uriFor('renderFormPage'),
'saveForm' => $this->controllerContext->getUriBuilder()->uriFor('saveForm')
],
'additionalViewModelModules' => $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']['additionalViewModelModules'],
'maximumUndoSteps' => $this->prototypeConfiguration['formEditor']['maximumUndoSteps'],
];
$this->view->assign('formEditorAppInitialData', json_encode($formEditorAppInitialData));
$this->view->assign('stylesheets', $this->resolveResourcePaths($this->prototypeConfiguration['formEditor']['stylesheets']));
$this->view->assign('formEditorTemplates', $this->renderFormEditorTemplates(
$this->prototypeConfiguration['formEditor']['formEditorTemplates'],
$formEditorDefinitions
));
$this->view->assign('dynamicRequireJsModules', $this->prototypeConfiguration['formEditor']['dynamicRequireJsModules']);
$popupWindowWidth = 700;
$popupWindowHeight = 750;
$popupWindowSize = ($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
? trim($this->getBackendUser()->getTSConfigVal('options.popupWindowSize'))
: null;
if (!empty($popupWindowSize)) {
list($popupWindowWidth, $popupWindowHeight) = GeneralUtility::intExplode('x', $popupWindowSize);
}
$addInlineSettings = [
'FormEditor' => [
'typo3WinBrowserUrl' => BackendUtility::getModuleUrl('wizard_element_browser'),
],
'Popup' => [
'PopupWindow' => [
'width' => $popupWindowWidth,
'height' => $popupWindowHeight
],
]
];
CoreArrayUtility::mergeRecursiveWithOverrule(
$addInlineSettings,
$this->prototypeConfiguration['formEditor']['addInlineSettings']
);
$this->view->assign('addInlineSettings', $addInlineSettings);
}
/**
* Save a formDefinition which was build by the form editor.
*
* @param string $formPersistenceIdentifier
* @param array $formDefinition
* @return string
* @internal
*/
public function saveFormAction(string $formPersistenceIdentifier, array $formDefinition): string
{
$formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
$formDefinition = $this->convertJsonArrayToAssociativeArray($formDefinition);
$this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
return '';
}
/**
* 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.
*
* @param array $formDefinition
* @param int $pageIndex
* @param string $prototypeName
* @return string
* @internal
*/
public function renderFormPageAction(array $formDefinition, int $pageIndex, string $prototypeName = null): string
{
$formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
$formDefinition = $this->convertJsonArrayToAssociativeArray($formDefinition);
if (empty($prototypeName)) {
$prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
}
$formFactory = $this->objectManager->get(ArrayFormFactory::class);
$formDefinition = $formFactory->build($formDefinition, $prototypeName);
$formDefinition->setRenderingOption('previewMode', true);
$form = $formDefinition->bind($this->request, $this->response);
$form->overrideCurrentPage($pageIndex);
return $form->render();
}
/**
* Prepare the formElements.*.formEditor section from the yaml settings.
* Sort all formElements into groups and add additional data.
*
* @param array $formElementsDefinition