Completed
Push — wip/steps ( f019be...82fcb6 )
by Romain
20:05
created

AjaxValidationController::errorAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 1
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\Controller;
15
16
use Exception;
17
use Romm\Formz\Controller\Processor\ControllerProcessor;
18
use Romm\Formz\Core\Core;
19
use Romm\Formz\Error\AjaxResult;
20
use Romm\Formz\Error\FormzMessageInterface;
21
use Romm\Formz\Exceptions\ClassNotFoundException;
22
use Romm\Formz\Exceptions\EntryNotFoundException;
23
use Romm\Formz\Exceptions\InvalidArgumentTypeException;
24
use Romm\Formz\Exceptions\InvalidArgumentValueException;
25
use Romm\Formz\Exceptions\InvalidConfigurationException;
26
use Romm\Formz\Exceptions\MissingArgumentException;
27
use Romm\Formz\Form\Definition\Field\Validation\Validator;
28
use Romm\Formz\Form\FormInterface;
29
use Romm\Formz\Form\FormObject\FormObject;
30
use Romm\Formz\Form\FormObject\FormObjectFactory;
31
use Romm\Formz\Middleware\Item\Begin\Service\FormService;
32
use Romm\Formz\Middleware\Processor\MiddlewareProcessor;
33
use Romm\Formz\Middleware\Request\Exception\StopPropagationException;
34
use Romm\Formz\Middleware\Scope\FieldValidationScope;
35
use Romm\Formz\Service\ContentObjectService;
36
use Romm\Formz\Service\ContextService;
37
use Romm\Formz\Service\ExtensionService;
38
use Romm\Formz\Service\MessageService;
39
use Romm\Formz\Validation\Field\DataObject\ValidatorDataObject;
40
use TYPO3\CMS\Core\Utility\GeneralUtility;
41
use TYPO3\CMS\Extbase\Error\Error;
42
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
43
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
44
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
45
use TYPO3\CMS\Extbase\Mvc\Web\Request;
46
use TYPO3\CMS\Extbase\Mvc\Web\Response;
47
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
48
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
49
50
class AjaxValidationController extends ActionController
51
{
52
    const DEFAULT_ERROR_MESSAGE_KEY = 'default_error_message';
53
    const MAPPING_ERROR = 'MappingError';
54
55
    /**
56
     * @var Request
57
     */
58
    protected $request;
59
60
    /**
61
     * @var Response
62
     */
63
    protected $response;
64
65
    /**
66
     * @var bool
67
     */
68
    protected $protectedRequestMode = true;
69
70
    /**
71
     * @var string
72
     */
73
    protected $formClassName;
74
75
    /**
76
     * @var string
77
     */
78
    protected $formName;
79
80
    /**
81
     * @var string
82
     */
83
    protected $fieldName;
84
85
    /**
86
     * @var string
87
     */
88
    protected $validatorName;
89
90
    /**
91
     * @var FormInterface
92
     */
93
    protected $form;
94
95
    /**
96
     * @var FormObject
97
     */
98
    protected $formObject;
99
100
    /**
101
     * @var AjaxResult
102
     */
103
    protected $result;
104
105
    /**
106
     * @var Validator
107
     */
108
    protected $validation;
109
110
    /**
111
     * The only accepted method for the request is `POST`.
112
     */
113
    public function initializeAction()
114
    {
115
        if ($this->request->getMethod() !== 'POST') {
116
            $this->throwStatus(400);
117
        }
118
    }
119
120
    /**
121
     * Will process the request, but also prevent any external message to be
122
     * displayed, and catch any exception that could occur during the
123
     * validation.
124
     *
125
     * @param RequestInterface  $request
126
     * @param ResponseInterface $response
127
     * @throws Exception
128
     */
129
    public function processRequest(RequestInterface $request, ResponseInterface $response)
130
    {
131
        $this->result = new AjaxResult;
132
133
        try {
134
            $this->processRequestParent($request, $response);
135
        } catch (Exception $exception) {
136
            if (false === $this->protectedRequestMode) {
137
                throw $exception;
138
            }
139
140
            $this->result->clear();
141
142
            $errorMessage = ExtensionService::get()->isInDebugMode()
143
                ? $this->getDebugMessageForException($exception)
144
                : ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY);
145
146
            $error = new Error($errorMessage, 1490176818);
147
            $this->result->addError($error);
148
            $this->result->setData('errorCode', $exception->getCode());
149
        }
150
151
        // Cleaning every external message.
152
        ob_clean();
153
154
        $this->injectResultInResponse();
155
    }
156
157
    /**
158
     * Will take care of adding a new argument to the request, based on the form
159
     * name and the form class name found in the request arguments.
160
     */
161
    protected function initializeActionMethodValidators()
162
    {
163
        $this->initializeActionMethodValidatorsParent();
164
165
        $request = $this->getRequest();
166
167
        if (false === $request->hasArgument('formzData')) {
168
            throw new \Exception('todo'); // @todo
169
        }
170
171
        if (false === $request->hasArgument('name')) {
172
            throw MissingArgumentException::ajaxControllerNameArgumentNotSet();
173
        }
174
175
        if (false === $request->hasArgument('className')) {
176
            throw MissingArgumentException::ajaxControllerClassNameArgumentNotSet();
177
        }
178
179
        $className = $request->getArgument('className');
180
181
        if (false === class_exists($className)) {
182
            throw ClassNotFoundException::ajaxControllerFormClassNameNotFound($className);
183
        }
184
185
        if (false === in_array(FormInterface::class, class_implements($className))) {
186
            throw InvalidArgumentTypeException::ajaxControllerWrongFormType($className);
187
        }
188
189
        $this->arguments->addNewArgument($request->getArgument('name'), $className, true);
190
    }
191
192
    /**
193
     * Main action that will process the field validation.
194
     *
195
     * @param string $name
196
     * @param string $className
197
     * @param string $fieldName
198
     * @param string $validatorName
199
     * @param string $formzData
200
     */
201
    public function runAction($name, $className, $fieldName, $validatorName, $formzData)
202
    {
203
        $this->formName = $name;
204
        $this->formClassName = $className;
205
        $this->fieldName = $fieldName;
206
        $this->validatorName = $validatorName;
207
        $this->form = $this->getForm();
208
209
        $this->formObject = $this->getFormObject();
210
211
        if ($formzData) {
212
            $this->formObject->getRequestData()->fillFromHash($formzData);
213
        }
214
215
        $this->invokeMiddlewares();
216
217
        $this->validation = $this->getFieldValidation();
218
219
        $validatorDataObject = new ValidatorDataObject($this->formObject, $this->validation);
220
221
        /** @var ValidatorInterface $validator */
222
        $validator = GeneralUtility::makeInstance(
223
            $this->validation->getClassName(),
224
            $this->validation->getOptions(),
225
            $validatorDataObject
226
        );
227
228
        $fieldValue = ObjectAccess::getProperty($this->form, $this->fieldName);
229
        $result = $validator->validate($fieldValue);
230
231
        $this->result->merge($result);
232
    }
233
234
    /**
235
     * If an error occurs, that must be because the mapping of the arguments
236
     * failed somehow. Therefore we override the default behaviour (forward to
237
     * referring request) and we throw an exception instead.
238
     *
239
     * @throws InvalidArgumentValueException
240
     */
241
    public function errorAction()
242
    {
243
        $this->signalSlotDispatcher->dispatch(
244
            __CLASS__,
245
            self::MAPPING_ERROR,
246
            [$this->arguments->getValidationResults()]
247
        );
248
249
        throw InvalidArgumentValueException::ajaxDataMapperError($this->arguments->getValidationResults()->getFlattenedErrors());
250
    }
251
252
    /**
253
     * Will call all middlewares of the form.
254
     *
255
     * Note that the field validation scope is used, meaning some middlewares
256
     * wont be called.
257
     *
258
     * @see \Romm\Formz\Middleware\Scope\FieldValidationScope
259
     */
260
    protected function invokeMiddlewares()
261
    {
262
        try {
263
            $definition = $this->formObject->getDefinition();
264
265
            if ($definition->hasSteps()) {
266
                $stepIdentifier = $this->formObject->getRequestData()->getCurrentStepIdentifier();
267
268
                if ($definition->getSteps()->hasEntry($stepIdentifier)) {
269
                    $step = $definition->getSteps()->getEntry($stepIdentifier);
270
271
                    $stepService = FormObjectFactory::get()->getStepService($this->formObject);
272
                    // @todo : dégueulasse
273
                    $stepService->fetchCurrentStep('Formz', 'AjaxValidation', 'run');
274
                    $stepService->setCurrentStep($step);
275
                }
276
            }
277
            $controllerProcessor = ControllerProcessor::prepare($this->request, $this->arguments, FieldValidationScope::class, $this->getContentObjectSettings());
278
279
            /** @var MiddlewareProcessor $middlewareProcessor */
280
            $middlewareProcessor = Core::instantiate(MiddlewareProcessor::class, $this->formObject, $controllerProcessor);
281
282
            $middlewareProcessor->run();
283
        } catch (StopPropagationException $exception) {
284
            // @todo exception if forward/redirect?
285
        }
286
    }
287
288
    /**
289
     * Will fetch the settings of the content object that was used to render the
290
     * form calling this controller.
291
     *
292
     * @return array
293
     */
294
    protected function getContentObjectSettings()
295
    {
296
        $referringRequest = $this->request->getReferringRequest();
297
298
        return ContentObjectService::get()->getContentObjectSettings(
299
            $this->formObject->getRequestData()->getContentObjectTable(),
300
            $this->formObject->getRequestData()->getContentObjectUid(),
301
            $referringRequest->getControllerExtensionName(),
302
            $referringRequest->getPluginName()
303
        );
304
    }
305
306
    /**
307
     * @return Validator
308
     * @throws EntryNotFoundException
309
     * @throws InvalidConfigurationException
310
     */
311
    protected function getFieldValidation()
312
    {
313
        $validationResult = $this->formObject->getDefinitionValidationResult();
314
315
        if (true === $validationResult->hasErrors()) {
316
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
317
        }
318
319
        $formConfiguration = $this->formObject->getDefinition();
320
321
        if (false === $formConfiguration->hasField($this->fieldName)) {
322
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
323
        }
324
325
        $field = $formConfiguration->getField($this->fieldName);
326
327
        if (false === $field->hasValidator($this->validatorName)) {
328
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
329
        }
330
331
        $fieldValidationConfiguration = $field->getValidator($this->validatorName);
332
333
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
334
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
335
        }
336
337
        return $fieldValidationConfiguration;
338
    }
339
340
    /**
341
     * Fetches errors/warnings/notices in the result, and put them in the JSON
342
     * response.
343
     */
344
    protected function injectResultInResponse()
345
    {
346
        $validationName = $this->validation instanceof Validator
347
            ? $this->validation->getName()
348
            : 'default';
349
350
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
351
352
        $result = [
353
            'success'  => !$this->result->hasErrors(),
354
            'data'     => $this->result->getData(),
355
            'messages' => [
356
                'errors'   => $this->formatMessages($validationResult->getErrors()),
357
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
358
                'notices'  => $this->formatMessages($validationResult->getNotices())
359
            ]
360
        ];
361
362
        $this->setUpResponseResult($result);
363
    }
364
365
    /**
366
     * @param array $result
367
     */
368
    protected function setUpResponseResult(array $result)
369
    {
370
        $this->response->setHeader('Content-Type', 'application/json');
371
        $this->response->setContent(json_encode($result));
372
373
        Core::get()->getPageController()->setContentType('application/json');
374
    }
375
376
    /**
377
     * @param FormzMessageInterface[] $messages
378
     * @return array
379
     */
380
    protected function formatMessages(array $messages)
381
    {
382
        $sortedMessages = [];
383
384
        foreach ($messages as $message) {
385
            $sortedMessages[$message->getMessageKey()] = $message->getMessage();
0 ignored issues
show
Bug introduced by
The method getMessage() does not exist on Romm\Formz\Error\FormzMessageInterface. Did you maybe mean getMessageKey()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
386
        }
387
388
        return $sortedMessages;
389
    }
390
391
    /**
392
     * Wrapper for unit tests.
393
     *
394
     * @param RequestInterface  $request
395
     * @param ResponseInterface $response
396
     */
397
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
398
    {
399
        parent::processRequest($request, $response);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (processRequest() instead of processRequestParent()). Are you sure this is correct? If so, you might want to change this to $this->processRequest().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
400
    }
401
402
    /**
403
     * Wrapper for unit tests.
404
     */
405
    protected function initializeActionMethodValidatorsParent()
406
    {
407
        parent::initializeActionMethodValidators();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (initializeActionMethodValidators() instead of initializeActionMethodValidatorsParent()). Are you sure this is correct? If so, you might want to change this to $this->initializeActionMethodValidators().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
408
    }
409
410
    /**
411
     * Used in unit testing.
412
     *
413
     * @param bool $flag
414
     */
415
    public function setProtectedRequestMode($flag)
416
    {
417
        $this->protectedRequestMode = (bool)$flag;
418
    }
419
420
    /**
421
     * @param Exception $exception
422
     * @return string
423
     */
424
    protected function getDebugMessageForException(Exception $exception)
425
    {
426
        return 'Debug mode – ' . $exception->getMessage();
427
    }
428
429
    /**
430
     * @return FormInterface
431
     * @throws MissingArgumentException
432
     */
433
    protected function getForm()
434
    {
435
        /** @var FormService $formService */
436
        $formService = Core::instantiate(FormService::class, $this->request, $this->arguments);
437
438
        return $formService->getFormInstance($this->formName);
439
    }
440
441
    /**
442
     * @return FormObject
443
     */
444
    protected function getFormObject()
445
    {
446
        return FormObjectFactory::get()->registerAndGetFormInstance($this->form, $this->formName);
447
    }
448
449
    /**
450
     * Wrapper for unit tests.
451
     *
452
     * @return Request
453
     */
454
    protected function getRequest()
455
    {
456
        return $this->request;
457
    }
458
}
459