Completed
Push — wip/steps ( a2d0c6...c3a71f )
by
unknown
02:22
created

AjaxValidationController::getDefaultErrorMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
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
                : $this->getDefaultErrorMessage();
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 get the default error message from the form configuartion
159
     */
160
    protected function getDefaultErrorMessage()
161
    {
162
        if ($this->formObject === null) {
163
            return ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY);
164
        }
165
166
        return $this->formObject->getDefinition()->getSettings()->getDefaultErrorMessage();
167
    }
168
169
    /**
170
     * Will take care of adding a new argument to the request, based on the form
171
     * name and the form class name found in the request arguments.
172
     */
173
    protected function initializeActionMethodValidators()
174
    {
175
        $this->initializeActionMethodValidatorsParent();
176
177
        $request = $this->getRequest();
178
179
        if (false === $request->hasArgument('formzData')) {
180
            throw new \Exception('todo'); // @todo
181
        }
182
183
        if (false === $request->hasArgument('name')) {
184
            throw MissingArgumentException::ajaxControllerNameArgumentNotSet();
185
        }
186
187
        if (false === $request->hasArgument('className')) {
188
            throw MissingArgumentException::ajaxControllerClassNameArgumentNotSet();
189
        }
190
191
        $className = $request->getArgument('className');
192
193
        if (false === class_exists($className)) {
194
            throw ClassNotFoundException::ajaxControllerFormClassNameNotFound($className);
195
        }
196
197
        if (false === in_array(FormInterface::class, class_implements($className))) {
198
            throw InvalidArgumentTypeException::ajaxControllerWrongFormType($className);
199
        }
200
201
        $this->arguments->addNewArgument($request->getArgument('name'), $className, true);
202
    }
203
204
    /**
205
     * Main action that will process the field validation.
206
     *
207
     * @param string $name
208
     * @param string $className
209
     * @param string $fieldName
210
     * @param string $validatorName
211
     * @param string $formzData
212
     */
213
    public function runAction($name, $className, $fieldName, $validatorName, $formzData)
214
    {
215
        $this->formName = $name;
216
        $this->formClassName = $className;
217
        $this->fieldName = $fieldName;
218
        $this->validatorName = $validatorName;
219
        $this->form = $this->getForm();
220
221
        $this->formObject = $this->getFormObject();
222
223
        if ($formzData) {
224
            $this->formObject->getRequestData()->fillFromHash($formzData);
225
        }
226
227
        $this->invokeMiddlewares();
228
229
        $this->validation = $this->getFieldValidation();
230
231
        $validatorDataObject = new ValidatorDataObject($this->formObject, $this->validation);
232
233
        /** @var ValidatorInterface $validator */
234
        $validator = GeneralUtility::makeInstance(
235
            $this->validation->getClassName(),
236
            $this->validation->getOptions(),
237
            $validatorDataObject
238
        );
239
240
        $fieldValue = ObjectAccess::getProperty($this->form, $this->fieldName);
241
        $result = $validator->validate($fieldValue);
242
243
        $this->result->merge($result);
244
    }
245
246
    /**
247
     * If an error occurs, that must be because the mapping of the arguments
248
     * failed somehow. Therefore we override the default behaviour (forward to
249
     * referring request) and we throw an exception instead.
250
     *
251
     * @throws InvalidArgumentValueException
252
     */
253
    public function errorAction()
254
    {
255
        $this->signalSlotDispatcher->dispatch(
256
            __CLASS__,
257
            self::MAPPING_ERROR,
258
            [$this->arguments->getValidationResults()]
259
        );
260
261
        throw InvalidArgumentValueException::ajaxDataMapperError($this->arguments->getValidationResults()->getFlattenedErrors());
262
    }
263
264
    /**
265
     * Will call all middlewares of the form.
266
     *
267
     * Note that the field validation scope is used, meaning some middlewares
268
     * wont be called.
269
     *
270
     * @see \Romm\Formz\Middleware\Scope\FieldValidationScope
271
     */
272
    protected function invokeMiddlewares()
273
    {
274
        try {
275
            $definition = $this->formObject->getDefinition();
276
277
            if ($definition->hasSteps()) {
278
                $stepIdentifier = $this->formObject->getRequestData()->getCurrentStepIdentifier();
279
280
                if ($definition->getSteps()->hasEntry($stepIdentifier)) {
281
                    $step = $definition->getSteps()->getEntry($stepIdentifier);
282
283
                    $stepService = FormObjectFactory::get()->getStepService($this->formObject);
284
                    // @todo : dégueulasse
285
                    $stepService->fetchCurrentStep('Formz', 'AjaxValidation', 'run');
286
                    $stepService->setCurrentStep($step);
287
                }
288
            }
289
            $controllerProcessor = ControllerProcessor::prepare($this->request, $this->arguments, FieldValidationScope::class, $this->getContentObjectSettings());
290
291
            /** @var MiddlewareProcessor $middlewareProcessor */
292
            $middlewareProcessor = Core::instantiate(MiddlewareProcessor::class, $this->formObject, $controllerProcessor);
293
294
            $middlewareProcessor->run();
295
        } catch (StopPropagationException $exception) {
296
            // @todo exception if forward/redirect?
297
        }
298
    }
299
300
    /**
301
     * Will fetch the settings of the content object that was used to render the
302
     * form calling this controller.
303
     *
304
     * @return array
305
     */
306
    protected function getContentObjectSettings()
307
    {
308
        $referringRequest = $this->request->getReferringRequest();
309
310
        return ContentObjectService::get()->getContentObjectSettings(
311
            $this->formObject->getRequestData()->getContentObjectTable(),
312
            $this->formObject->getRequestData()->getContentObjectUid(),
313
            $referringRequest->getControllerExtensionName(),
314
            $referringRequest->getPluginName()
315
        );
316
    }
317
318
    /**
319
     * @return Validator
320
     * @throws EntryNotFoundException
321
     * @throws InvalidConfigurationException
322
     */
323
    protected function getFieldValidation()
324
    {
325
        $validationResult = $this->formObject->getDefinitionValidationResult();
326
327
        if (true === $validationResult->hasErrors()) {
328
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
329
        }
330
331
        $formConfiguration = $this->formObject->getDefinition();
332
333
        if (false === $formConfiguration->hasField($this->fieldName)) {
334
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
335
        }
336
337
        $field = $formConfiguration->getField($this->fieldName);
338
339
        if (false === $field->hasValidator($this->validatorName)) {
340
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
341
        }
342
343
        $fieldValidationConfiguration = $field->getValidator($this->validatorName);
344
345
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
346
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
347
        }
348
349
        return $fieldValidationConfiguration;
350
    }
351
352
    /**
353
     * Fetches errors/warnings/notices in the result, and put them in the JSON
354
     * response.
355
     */
356
    protected function injectResultInResponse()
357
    {
358
        $validationName = $this->validation instanceof Validator
359
            ? $this->validation->getName()
360
            : 'default';
361
362
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
363
364
        $result = [
365
            'success'  => !$this->result->hasErrors(),
366
            'data'     => $this->result->getData(),
367
            'messages' => [
368
                'errors'   => $this->formatMessages($validationResult->getErrors()),
369
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
370
                'notices'  => $this->formatMessages($validationResult->getNotices())
371
            ]
372
        ];
373
374
        $this->setUpResponseResult($result);
375
    }
376
377
    /**
378
     * @param array $result
379
     */
380
    protected function setUpResponseResult(array $result)
381
    {
382
        $this->response->setHeader('Content-Type', 'application/json');
383
        $this->response->setContent(json_encode($result));
384
385
        Core::get()->getPageController()->setContentType('application/json');
386
    }
387
388
    /**
389
     * @param FormzMessageInterface[] $messages
390
     * @return array
391
     */
392
    protected function formatMessages(array $messages)
393
    {
394
        $sortedMessages = [];
395
396
        foreach ($messages as $message) {
397
            $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...
398
        }
399
400
        return $sortedMessages;
401
    }
402
403
    /**
404
     * Wrapper for unit tests.
405
     *
406
     * @param RequestInterface  $request
407
     * @param ResponseInterface $response
408
     */
409
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
410
    {
411
        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...
412
    }
413
414
    /**
415
     * Wrapper for unit tests.
416
     */
417
    protected function initializeActionMethodValidatorsParent()
418
    {
419
        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...
420
    }
421
422
    /**
423
     * Used in unit testing.
424
     *
425
     * @param bool $flag
426
     */
427
    public function setProtectedRequestMode($flag)
428
    {
429
        $this->protectedRequestMode = (bool)$flag;
430
    }
431
432
    /**
433
     * @param Exception $exception
434
     * @return string
435
     */
436
    protected function getDebugMessageForException(Exception $exception)
437
    {
438
        return 'Debug mode – ' . $exception->getMessage();
439
    }
440
441
    /**
442
     * @return FormInterface
443
     * @throws MissingArgumentException
444
     */
445
    protected function getForm()
446
    {
447
        /** @var FormService $formService */
448
        $formService = Core::instantiate(FormService::class, $this->request, $this->arguments);
449
450
        return $formService->getFormInstance($this->formName);
451
    }
452
453
    /**
454
     * @return FormObject
455
     */
456
    protected function getFormObject()
457
    {
458
        return FormObjectFactory::get()->registerAndGetFormInstance($this->form, $this->formName);
459
    }
460
461
    /**
462
     * Wrapper for unit tests.
463
     *
464
     * @return Request
465
     */
466
    protected function getRequest()
467
    {
468
        return $this->request;
469
    }
470
}
471