Completed
Push — bugfix/SUN-4277-formz-error-aj... ( 340776 )
by Romain
36:59
created

AjaxValidationController::errorAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
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
54
    /**
55
     * @var Request
56
     */
57
    protected $request;
58
59
    /**
60
     * @var Response
61
     */
62
    protected $response;
63
64
    /**
65
     * @var bool
66
     */
67
    protected $protectedRequestMode = true;
68
69
    /**
70
     * @var string
71
     */
72
    protected $formClassName;
73
74
    /**
75
     * @var string
76
     */
77
    protected $formName;
78
79
    /**
80
     * @var string
81
     */
82
    protected $fieldName;
83
84
    /**
85
     * @var string
86
     */
87
    protected $validatorName;
88
89
    /**
90
     * @var FormInterface
91
     */
92
    protected $form;
93
94
    /**
95
     * @var FormObject
96
     */
97
    protected $formObject;
98
99
    /**
100
     * @var AjaxResult
101
     */
102
    protected $result;
103
104
    /**
105
     * @var Validator
106
     */
107
    protected $validation;
108
109
    /**
110
     * The only accepted method for the request is `POST`.
111
     */
112
    public function initializeAction()
113
    {
114
        if ($this->request->getMethod() !== 'POST') {
115
            $this->throwStatus(400);
116
        }
117
    }
118
119
    /**
120
     * Will process the request, but also prevent any external message to be
121
     * displayed, and catch any exception that could occur during the
122
     * validation.
123
     *
124
     * @param RequestInterface  $request
125
     * @param ResponseInterface $response
126
     * @throws Exception
127
     */
128
    public function processRequest(RequestInterface $request, ResponseInterface $response)
129
    {
130
        $this->result = new AjaxResult;
131
132
        try {
133
            $this->processRequestParent($request, $response);
134
        } catch (Exception $exception) {
135
            if (false === $this->protectedRequestMode) {
136
                throw $exception;
137
            }
138
139
            $this->result->clear();
140
141
            $errorMessage = ExtensionService::get()->isInDebugMode()
142
                ? $this->getDebugMessageForException($exception)
143
                : ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY);
144
145
            $error = new Error($errorMessage, 1490176818);
146
            $this->result->addError($error);
147
            $this->result->setData('errorCode', $exception->getCode());
148
        }
149
150
        // Cleaning every external message.
151
        ob_clean();
152
153
        $this->injectResultInResponse();
154
    }
155
156
    /**
157
     * Will take care of adding a new argument to the request, based on the form
158
     * name and the form class name found in the request arguments.
159
     */
160
    protected function initializeActionMethodValidators()
161
    {
162
        $this->initializeActionMethodValidatorsParent();
163
164
        $request = $this->getRequest();
165
166
        if (false === $request->hasArgument('formzData')) {
167
            throw new \Exception('todo'); // @todo
168
        }
169
170
        if (false === $request->hasArgument('name')) {
171
            throw MissingArgumentException::ajaxControllerNameArgumentNotSet();
172
        }
173
174
        if (false === $request->hasArgument('className')) {
175
            throw MissingArgumentException::ajaxControllerClassNameArgumentNotSet();
176
        }
177
178
        $className = $request->getArgument('className');
179
180
        if (false === class_exists($className)) {
181
            throw ClassNotFoundException::ajaxControllerFormClassNameNotFound($className);
182
        }
183
184
        if (false === in_array(FormInterface::class, class_implements($className))) {
185
            throw InvalidArgumentTypeException::ajaxControllerWrongFormType($className);
186
        }
187
188
        $this->arguments->addNewArgument($request->getArgument('name'), $className, true);
189
    }
190
191
    /**
192
     * Main action that will process the field validation.
193
     *
194
     * @param string $name
195
     * @param string $className
196
     * @param string $fieldName
197
     * @param string $validatorName
198
     * @param string $formzData
199
     */
200
    public function runAction($name, $className, $fieldName, $validatorName, $formzData)
201
    {
202
        $this->formName = $name;
203
        $this->formClassName = $className;
204
        $this->fieldName = $fieldName;
205
        $this->validatorName = $validatorName;
206
        $this->form = $this->getForm();
207
208
        $this->formObject = $this->getFormObject();
209
210
        if ($formzData) {
211
            $this->formObject->getRequestData()->fillFromHash($formzData);
212
        }
213
214
        $this->invokeMiddlewares();
215
216
        $this->validation = $this->getFieldValidation();
217
218
        $validatorDataObject = new ValidatorDataObject($this->formObject, $this->validation);
219
220
        /** @var ValidatorInterface $validator */
221
        $validator = GeneralUtility::makeInstance(
222
            $this->validation->getClassName(),
223
            $this->validation->getOptions(),
224
            $validatorDataObject
225
        );
226
227
        $fieldValue = ObjectAccess::getProperty($this->form, $this->fieldName);
228
        $result = $validator->validate($fieldValue);
229
230
        $this->result->merge($result);
231
    }
232
233
    /**
234
     * If an error occurs, that must be because the mapping of the arguments
235
     * failed somehow. Therefore we override the default behaviour (forward to
236
     * referring request) and we throw an exception instead.
237
     *
238
     * @throws InvalidArgumentValueException
239
     */
240
    public function errorAction()
241
    {
242
        throw InvalidArgumentValueException::ajaxDataMapperError($this->arguments->getValidationResults()->getFlattenedErrors());
243
    }
244
245
    /**
246
     * Will call all middlewares of the form.
247
     *
248
     * Note that the field validation scope is used, meaning some middlewares
249
     * wont be called.
250
     *
251
     * @see \Romm\Formz\Middleware\Scope\FieldValidationScope
252
     */
253
    protected function invokeMiddlewares()
254
    {
255
        try {
256
            $definition = $this->formObject->getDefinition();
257
258
            if ($definition->hasSteps()) {
259
                $stepIdentifier = $this->formObject->getRequestData()->getCurrentStepIdentifier();
260
261
                if ($definition->getSteps()->hasEntry($stepIdentifier)) {
262
                    $step = $definition->getSteps()->getEntry($stepIdentifier);
263
264
                    $stepService = FormObjectFactory::get()->getStepService($this->formObject);
265
                    // @todo : dégueulasse
266
                    $stepService->fetchCurrentStep('Formz', 'AjaxValidation', 'run');
267
                    $stepService->setCurrentStep($step);
268
                }
269
            }
270
            $controllerProcessor = ControllerProcessor::prepare($this->request, $this->arguments, FieldValidationScope::class, $this->getContentObjectSettings());
271
272
            /** @var MiddlewareProcessor $middlewareProcessor */
273
            $middlewareProcessor = Core::instantiate(MiddlewareProcessor::class, $this->formObject, $controllerProcessor);
274
275
            $middlewareProcessor->run();
276
        } catch (StopPropagationException $exception) {
277
            // @todo exception if forward/redirect?
278
        }
279
    }
280
281
    /**
282
     * Will fetch the settings of the content object that was used to render the
283
     * form calling this controller.
284
     *
285
     * @return array
286
     */
287
    protected function getContentObjectSettings()
288
    {
289
        $referringRequest = $this->request->getReferringRequest();
290
291
        return ContentObjectService::get()->getContentObjectSettings(
292
            $this->formObject->getRequestData()->getContentObjectTable(),
293
            $this->formObject->getRequestData()->getContentObjectUid(),
294
            $referringRequest->getControllerExtensionName(),
295
            $referringRequest->getPluginName()
296
        );
297
    }
298
299
    /**
300
     * @return Validator
301
     * @throws EntryNotFoundException
302
     * @throws InvalidConfigurationException
303
     */
304
    protected function getFieldValidation()
305
    {
306
        $validationResult = $this->formObject->getDefinitionValidationResult();
307
308
        if (true === $validationResult->hasErrors()) {
309
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
310
        }
311
312
        $formConfiguration = $this->formObject->getDefinition();
313
314
        if (false === $formConfiguration->hasField($this->fieldName)) {
315
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
316
        }
317
318
        $field = $formConfiguration->getField($this->fieldName);
319
320
        if (false === $field->hasValidator($this->validatorName)) {
321
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
322
        }
323
324
        $fieldValidationConfiguration = $field->getValidator($this->validatorName);
325
326
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
327
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
328
        }
329
330
        return $fieldValidationConfiguration;
331
    }
332
333
    /**
334
     * Fetches errors/warnings/notices in the result, and put them in the JSON
335
     * response.
336
     */
337
    protected function injectResultInResponse()
338
    {
339
        $validationName = $this->validation instanceof Validator
340
            ? $this->validation->getName()
341
            : 'default';
342
343
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
344
345
        $result = [
346
            'success'  => !$this->result->hasErrors(),
347
            'data'     => $this->result->getData(),
348
            'messages' => [
349
                'errors'   => $this->formatMessages($validationResult->getErrors()),
350
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
351
                'notices'  => $this->formatMessages($validationResult->getNotices())
352
            ]
353
        ];
354
355
        $this->setUpResponseResult($result);
356
    }
357
358
    /**
359
     * @param array $result
360
     */
361
    protected function setUpResponseResult(array $result)
362
    {
363
        $this->response->setHeader('Content-Type', 'application/json');
364
        $this->response->setContent(json_encode($result));
365
366
        Core::get()->getPageController()->setContentType('application/json');
367
    }
368
369
    /**
370
     * @param FormzMessageInterface[] $messages
371
     * @return array
372
     */
373
    protected function formatMessages(array $messages)
374
    {
375
        $sortedMessages = [];
376
377
        foreach ($messages as $message) {
378
            $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...
379
        }
380
381
        return $sortedMessages;
382
    }
383
384
    /**
385
     * Wrapper for unit tests.
386
     *
387
     * @param RequestInterface  $request
388
     * @param ResponseInterface $response
389
     */
390
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
391
    {
392
        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...
393
    }
394
395
    /**
396
     * Wrapper for unit tests.
397
     */
398
    protected function initializeActionMethodValidatorsParent()
399
    {
400
        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...
401
    }
402
403
    /**
404
     * Used in unit testing.
405
     *
406
     * @param bool $flag
407
     */
408
    public function setProtectedRequestMode($flag)
409
    {
410
        $this->protectedRequestMode = (bool)$flag;
411
    }
412
413
    /**
414
     * @param Exception $exception
415
     * @return string
416
     */
417
    protected function getDebugMessageForException(Exception $exception)
418
    {
419
        return 'Debug mode – ' . $exception->getMessage();
420
    }
421
422
    /**
423
     * @return FormInterface
424
     * @throws MissingArgumentException
425
     */
426
    protected function getForm()
427
    {
428
        /** @var FormService $formService */
429
        $formService = Core::instantiate(FormService::class, $this->request, $this->arguments);
430
431
        return $formService->getFormInstance($this->formName);
432
    }
433
434
    /**
435
     * @return FormObject
436
     */
437
    protected function getFormObject()
438
    {
439
        return FormObjectFactory::get()->registerAndGetFormInstance($this->form, $this->formName);
440
    }
441
442
    /**
443
     * Wrapper for unit tests.
444
     *
445
     * @return Request
446
     */
447
    protected function getRequest()
448
    {
449
        return $this->request;
450
    }
451
}
452