Completed
Push — middleware-wip ( 3d734d...55159c )
by Romain
02:44
created

FormViewHelper::injectFormRequestData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
/*
3
 * 2017 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 FormZ project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\Formz\ViewHelpers;
15
16
use Romm\Formz\AssetHandler\AssetHandlerFactory;
17
use Romm\Formz\AssetHandler\Connector\AssetHandlerConnectorManager;
18
use Romm\Formz\AssetHandler\Html\DataAttributesAssetHandler;
19
use Romm\Formz\Core\Core;
20
use Romm\Formz\Exceptions\ClassNotFoundException;
21
use Romm\Formz\Exceptions\EntryNotFoundException;
22
use Romm\Formz\Exceptions\InvalidOptionValueException;
23
use Romm\Formz\Form\FormInterface;
24
use Romm\Formz\Form\FormObject;
25
use Romm\Formz\Form\FormObjectFactory;
26
use Romm\Formz\Service\ContextService;
27
use Romm\Formz\Service\ControllerService;
28
use Romm\Formz\Service\ExtensionService;
29
use Romm\Formz\Service\StringService;
30
use Romm\Formz\Service\TimeTrackerService;
31
use Romm\Formz\Service\ViewHelper\FormViewHelperService;
32
use Romm\Formz\Validation\Validator\Form\DefaultFormValidator;
33
use TYPO3\CMS\Core\Page\PageRenderer;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
36
use TYPO3\CMS\Fluid\View\StandaloneView;
37
38
/**
39
 * This view helper overrides the default one from Extbase, to include
40
 * everything the extension needs to work properly.
41
 *
42
 * The only difference in Fluid is that the attribute "name" becomes mandatory,
43
 * and must be the exact same name as the form parameter in the controller
44
 * action called when the form is submitted. For instance, if your action looks
45
 * like this: `public function submitAction(ExampleForm $exampleForm) {...}`,
46
 * then the "name" attribute of this view helper must be "exampleForm".
47
 *
48
 * Thanks to the information of the form, the following things are automatically
49
 * handled in this view helper:
50
 *
51
 * - Class
52
 *   A custom class may be added to the form DOM element. If the TypoScript
53
 *   configuration "settings.defaultClass" is set for this form, then the given
54
 *   class will be added to the form element.
55
 *
56
 * - JavaScript
57
 *   A block of JavaScript is built from scratch, which will initialize the
58
 *   form, add validation rules to the fields, and handle activation of the
59
 *   fields validation.
60
 *
61
 * - Data attributes
62
 *   To help integrators customize every aspect they need in CSS, every useful
63
 *   information is put in data attributes in the form DOM element. For example,
64
 *   you can know in real time if the field "email" is valid if the form has the
65
 *   attribute "fz-valid-email"
66
 *
67
 * - CSS
68
 *   A block of CSS is built from scratch, which will handle the fields display,
69
 *   depending on their activation property.
70
 */
71
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
72
{
73
    /**
74
     * @var bool
75
     */
76
    protected $escapeOutput = false;
77
78
    /**
79
     * @var PageRenderer
80
     */
81
    protected $pageRenderer;
82
83
    /**
84
     * @var FormObject
85
     */
86
    protected $formObject;
87
88
    /**
89
     * @var FormViewHelperService
90
     */
91
    protected $formService;
92
93
    /**
94
     * @var string
95
     */
96
    protected $formObjectClassName;
97
98
    /**
99
     * @var AssetHandlerFactory
100
     */
101
    protected $assetHandlerFactory;
102
103
    /**
104
     * @var TimeTrackerService
105
     */
106
    protected $timeTracker;
107
108
    /**
109
     * @var bool
110
     */
111
    protected $typoScriptIncluded = false;
112
113
    /**
114
     * @var ControllerService
115
     */
116
    protected $controllerService;
117
118
    /**
119
     * @inheritdoc
120
     */
121
    public function initialize()
122
    {
123
        parent::initialize();
124
125
        $this->typoScriptIncluded = ContextService::get()->isTypoScriptIncluded();
126
127
        if (true === $this->typoScriptIncluded) {
128
            $this->timeTracker = TimeTrackerService::getAndStart();
129
            $this->formObjectClassName = $this->getFormClassName();
130
            $this->formObject = $this->getFormObject();
131
            $this->timeTracker->logTime('post-config');
132
            $this->formService->setFormObject($this->formObject);
133
            $this->assetHandlerFactory = AssetHandlerFactory::get($this->formObject, $this->controllerContext);
134
135
            $this->injectFormRequestData();
136
137
            /*
138
             * If the argument `object` was filled with an instance of Form, it
139
             * is added to the `FormObject`.
140
             */
141
            $objectArgument = $this->arguments['object'];
142
143
            if ($objectArgument instanceof FormInterface
144
                && false === $this->formObject->formWasSubmitted()
145
            ) {
146
                $this->formObject->setForm($objectArgument);
147
            }
148
        }
149
150
        /*
151
         * Important: we need to instantiate the page renderer with this instead
152
         * of Extbase object manager (or with an inject function).
153
         *
154
         * This is due to some TYPO3 low level behaviour which overrides the
155
         * page renderer singleton instance, whenever a new request is used. The
156
         * problem is that the instance is not updated on Extbase side.
157
         *
158
         * Using Extbase injection can lead to old page renderer instance being
159
         * used, resulting in a leak of assets inclusion, and maybe more issues.
160
         */
161
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
162
    }
163
164
    /**
165
     * @inheritdoc
166
     */
167
    public function initializeArguments()
168
    {
169
        parent::initializeArguments();
170
171
        // The name attribute becomes mandatory.
172
        $this->overrideArgument('name', 'string', 'Name of the form.', true);
173
        $this->registerArgument('formClassName', 'string', 'Class name of the form.', false);
174
    }
175
176
    /**
177
     * @return string
178
     */
179
    protected function renderViewHelper()
180
    {
181
        if (false === $this->typoScriptIncluded) {
182
            return (ExtensionService::get()->isInDebugMode())
183
                ? ContextService::get()->translate('form.typoscript_not_included.error_message')
184
                : '';
185
        }
186
187
        $result = ($this->formObject->getConfigurationValidationResult()->hasErrors())
188
            // If the form configuration is not valid, we display the errors list.
189
            ? $this->getErrorText()
190
            // Everything is ok, we render the form.
191
            : $this->renderForm(func_get_args());
192
193
        $this->timeTracker->logTime('final');
194
195
        if (ExtensionService::get()->isInDebugMode()) {
196
            $result = $this->timeTracker->getHTMLCommentLogs() . LF . $result;
197
        }
198
199
        $this->formService->resetState();
200
201
        return $result;
202
    }
203
204
    /**
205
     * Will render the whole form and return the HTML result.
206
     *
207
     * @param array $arguments
208
     * @return string
209
     */
210
    final protected function renderForm(array $arguments)
211
    {
212
        /*
213
         * We begin by setting up the form service: request results and form
214
         * instance are inserted in the service, and are used afterwards.
215
         *
216
         * There are only two ways to be sure the values injected are correct:
217
         * when the form was actually submitted by the user, or when the
218
         * argument `object` of the view helper is filled with a form instance.
219
         */
220
        $this->formService->activateFormContext();
221
222
        /*
223
         * If the form was submitted, applying custom behaviours on its fields.
224
         */
225
        $this->formService->applyBehavioursOnSubmittedForm($this->controllerContext);
226
227
        /*
228
         * Adding the default class configured in TypoScript configuration to
229
         * the form HTML tag.
230
         */
231
        $this->addDefaultClass();
232
233
        /*
234
         * Handling data attributes that are added to the form HTML tag,
235
         * depending on several parameters.
236
         */
237
        $this->handleDataAttributes();
238
239
        /*
240
         * Including JavaScript and CSS assets in the page renderer.
241
         */
242
        $this->handleAssets();
243
244
        $this->timeTracker->logTime('pre-render');
245
246
        /*
247
         * Getting the result of the original Fluid `FormViewHelper` rendering.
248
         */
249
        $result = $this->getParentRenderResult($arguments);
250
251
        /*
252
         * Language files need to be included at the end, because they depend on
253
         * what was used by previous assets.
254
         */
255
        $this->getAssetHandlerConnectorManager()
256
            ->getJavaScriptAssetHandlerConnector()
257
            ->includeLanguageJavaScriptFiles();
258
259
        return $result;
260
    }
261
262
    /**
263
     * @todo
264
     */
265
    protected function injectFormRequestData()
266
    {
267
        /** @var ConfigurationManager $configurationManager */
268
        $configurationManager = $this->objectManager->get(ConfigurationManager::class);
269
270
        $contentObject = $configurationManager->getContentObject();
271
        $requestData = $this->formObject->getRequestData();
272
273
        $requestData->setContentObjectTable($contentObject->getCurrentTable());
274
        $requestData->setContentObjectUid($contentObject->data['uid']);
275
    }
276
277
    /**
278
     * @todo
279
     *
280
     * @return string
281
     */
282
    protected function renderHiddenReferrerFields()
283
    {
284
        $result = parent::renderHiddenReferrerFields();
285
286
        $requestData = $this->formObject->getRequestData()->toArray();
287
        $value = htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($requestData))));
288
289
        $result .= '<input type="hidden" name="' . $this->prefixFieldName('formzData') . '" value="' . $value . '" />' . LF;
290
291
        return $result;
292
    }
293
294
    /**
295
     * Will add a default class to the form element.
296
     *
297
     * To customize the class, take a look at `settings.defaultClass` in the
298
     * form TypoScript configuration.
299
     */
300
    protected function addDefaultClass()
301
    {
302
        $formDefaultClass = $this->formObject
303
            ->getConfiguration()
304
            ->getSettings()
305
            ->getDefaultClass();
306
307
        $class = $this->tag->getAttribute('class');
308
309
        if (false === empty($formDefaultClass)) {
310
            $class = (!empty($class) ? $class . ' ' : '') . $formDefaultClass;
311
            $this->tag->addAttribute('class', $class);
312
        }
313
    }
314
315
    /**
316
     * Adds custom data attributes to the form element, based on the
317
     * submitted form values and results.
318
     */
319
    protected function handleDataAttributes()
320
    {
321
        $dataAttributes = [];
322
323
        $dataAttributesAssetHandler = $this->getDataAttributesAssetHandler();
324
325
        if ($this->formObject->hasForm()) {
326
            if (false === $this->formObject->hasFormResult()) {
327
                $form = $this->formObject->getForm();
328
                $formValidator = $this->getFormValidator($this->getFormObjectName());
329
                $formResult = $formValidator->validateGhost($form);
330
            } else {
331
                $formResult = $this->formObject->getFormResult();
332
            }
333
334
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValuesDataAttributes($formResult);
335
        }
336
337
        if (true === $this->formObject->formWasSubmitted()) {
338
            $dataAttributes += [DataAttributesAssetHandler::getFieldSubmissionDone() => '1'];
339
            $dataAttributes += $dataAttributesAssetHandler->getFieldsValidDataAttributes();
340
            $dataAttributes += $dataAttributesAssetHandler->getFieldsMessagesDataAttributes();
341
        }
342
343
        $this->tag->addAttributes($dataAttributes);
344
    }
345
346
    /**
347
     * Will include all JavaScript and CSS assets needed for this form.
348
     */
349
    protected function handleAssets()
350
    {
351
        $assetHandlerConnectorManager = $this->getAssetHandlerConnectorManager();
352
353
        // Default FormZ assets.
354
        $assetHandlerConnectorManager->includeDefaultAssets();
355
356
        // JavaScript assets.
357
        $assetHandlerConnectorManager->getJavaScriptAssetHandlerConnector()
358
            ->generateAndIncludeFormzConfigurationJavaScript()
359
            ->generateAndIncludeJavaScript()
360
            ->generateAndIncludeInlineJavaScript()
361
            ->includeJavaScriptValidationAndConditionFiles();
362
363
        // CSS assets.
364
        $assetHandlerConnectorManager->getCssAssetHandlerConnector()
365
            ->includeGeneratedCss();
366
    }
367
368
    /**
369
     * Will return an error text from a Fluid view.
370
     *
371
     * @return string
372
     */
373
    protected function getErrorText()
374
    {
375
        /** @var $view StandaloneView */
376
        $view = Core::instantiate(StandaloneView::class);
377
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:' . ExtensionService::get()->getExtensionKey() . '/Resources/Private/Templates/Error/ConfigurationErrorBlock.html'));
378
        $layoutRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Layouts');
379
        $partialRootPath = StringService::get()->getExtensionRelativePath('Resources/Private/Partials');
380
        $view->setLayoutRootPaths([$layoutRootPath]);
381
        $view->setPartialRootPaths([$partialRootPath]);
382
        $view->assign('formObject', $this->formObject);
383
384
        return $view->render();
385
    }
386
387
    /**
388
     * Returns the class name of the form object: it is fetched from the action
389
     * of the controller which will be called when submitting this form. It
390
     * means two things:
391
     * - The action must have a parameter which has the exact same name as the
392
     *   form;
393
     * - The parameter must indicate its type.
394
     *
395
     * @return string
396
     * @throws ClassNotFoundException
397
     * @throws InvalidOptionValueException
398
     */
399
    protected function getFormClassName()
400
    {
401
        $formClassName = ($this->hasArgument('formClassName'))
402
            ? $this->arguments['formClassName']
403
            : $this->getFormClassNameFromControllerAction();
404
405
        if (false === class_exists($formClassName)) {
406
            throw ClassNotFoundException::formViewHelperClassNotFound($formClassName, $this->getFormObjectName(), $this->getControllerName(), $this->getControllerActionName());
407
        }
408
409
        if (false === in_array(FormInterface::class, class_implements($formClassName))) {
410
            throw InvalidOptionValueException::formViewHelperWrongFormType($formClassName);
411
        }
412
413
        return $formClassName;
414
    }
415
416
    /**
417
     * Will fetch the name of the controller action argument bound to this
418
     * request.
419
     *
420
     * @return string
421
     * @throws EntryNotFoundException
422
     */
423
    protected function getFormClassNameFromControllerAction()
424
    {
425
        return $this->controllerService->getFormClassNameFromControllerAction(
426
            $this->getControllerName(),
427
            $this->getControllerActionName(),
428
            $this->getFormObjectName()
429
        );
430
    }
431
432
    /**
433
     * Renders the whole Fluid template.
434
     *
435
     * @param array $arguments
436
     * @return string
437
     */
438
    protected function getParentRenderResult(array $arguments)
439
    {
440
        return call_user_func_array([get_parent_class(), 'render'], $arguments);
441
    }
442
443
    /**
444
     * @return string
445
     */
446
    protected function getControllerName()
447
    {
448
        return ($this->arguments['controller'])
449
            ?: $this->controllerContext
450
                ->getRequest()
451
                ->getControllerObjectName();
452
    }
453
454
    /**
455
     * @return string
456
     */
457
    protected function getControllerActionName()
458
    {
459
        return ($this->arguments['action'])
460
            ?: $this->controllerContext
461
                ->getRequest()
462
                ->getControllerActionName();
463
    }
464
465
    /**
466
     * @param string $formName
467
     * @return DefaultFormValidator
468
     */
469
    protected function getFormValidator($formName)
470
    {
471
        /** @var DefaultFormValidator $validation */
472
        $validation = Core::instantiate(DefaultFormValidator::class, ['name' => $formName]);
473
474
        return $validation;
475
    }
476
477
    /**
478
     * @return AssetHandlerConnectorManager
479
     */
480
    protected function getAssetHandlerConnectorManager()
481
    {
482
        return AssetHandlerConnectorManager::get($this->pageRenderer, $this->assetHandlerFactory);
483
    }
484
485
    /**
486
     * @return DataAttributesAssetHandler
487
     */
488
    protected function getDataAttributesAssetHandler()
489
    {
490
        /** @var DataAttributesAssetHandler $assetHandler */
491
        $assetHandler = $this->assetHandlerFactory->getAssetHandler(DataAttributesAssetHandler::class);
492
493
        return $assetHandler;
494
    }
495
496
    /**
497
     * @return FormObject
498
     */
499
    protected function getFormObject()
500
    {
501
        /** @var FormObjectFactory $formObjectFactory */
502
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
503
504
        return $formObjectFactory->getInstanceFromClassName($this->formObjectClassName, $this->getFormObjectName());
505
    }
506
507
    /**
508
     * @param FormViewHelperService $service
509
     */
510
    public function injectFormService(FormViewHelperService $service)
511
    {
512
        $this->formService = $service;
513
    }
514
515
    /**
516
     * @param ControllerService $controllerService
517
     */
518
    public function injectControllerService(ControllerService $controllerService)
519
    {
520
        $this->controllerService = $controllerService;
521
    }
522
}
523