Completed
Push — feature/middleware-tmp ( 8f1e4d )
by Romain
01:57
created

AjaxValidationController   D

Complexity

Total Complexity 33

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 24
dl 0
loc 356
rs 4.9238
c 0
b 0
f 0

16 Methods

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