Completed
Push — feature/middleware ( 864c18 )
by Romain
03:21
created

AjaxValidationController   D

Complexity

Total Complexity 34

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 25

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 25
dl 0
loc 375
rs 4.6
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getRequest() 0 4 1
A initializeAction() 0 6 2
B processRequest() 0 27 4
B initializeActionMethodValidators() 0 30 6
B runAction() 0 32 2
A getContentObjectSettings() 0 12 1
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 getDebugMessageForException() 0 4 1
A getForm() 0 4 1
A getFormObject() 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 fetch the settings of the content object that was used to render the
232
     * form calling this controller.
233
     *
234
     * @return array
235
     */
236
    protected function getContentObjectSettings()
237
    {
238
        $requestData = $this->formObject->getRequestData();
239
        $referringRequest = $this->request->getReferringRequest();
240
241
        return ContentObjectService::get()->getContentObjectSettings(
242
            $requestData->getContentObjectTable(),
243
            $requestData->getContentObjectUid(),
244
            $referringRequest->getControllerExtensionName(),
245
            $referringRequest->getPluginName()
246
        );
247
    }
248
249
    /**
250
     * Will call all middlewares of the form.
251
     *
252
     * Note that the "single field validation context" is activated, meaning
253
     * some middlewares wont be called.
254
     *
255
     * @see \Romm\Formz\Middleware\Processor\RemoveFromSingleFieldValidationContext
256
     */
257
    protected function invokeMiddlewares()
258
    {
259
        try {
260
            $controllerProcessor = ControllerProcessor::prepare($this->request, $this->arguments, $this->getContentObjectSettings());
261
262
            /** @var MiddlewareProcessor $middlewareProcessor */
263
            $middlewareProcessor = Core::instantiate(MiddlewareProcessor::class, $this->formObject, $controllerProcessor);
264
265
            $middlewareProcessor->activateSingleFieldValidationContext();
266
            $middlewareProcessor->run();
267
        } catch (StopPropagationException $exception) {
268
            // @todo exception if forward/redirect?
269
        }
270
    }
271
272
    /**
273
     * @return Validator
274
     * @throws EntryNotFoundException
275
     * @throws InvalidConfigurationException
276
     */
277
    protected function getFieldValidation()
278
    {
279
        $validationResult = $this->formObject->getDefinitionValidationResult();
280
281
        if (true === $validationResult->hasErrors()) {
282
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
283
        }
284
285
        $formConfiguration = $this->formObject->getDefinition();
286
287
        if (false === $formConfiguration->hasField($this->fieldName)) {
288
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
289
        }
290
291
        $field = $formConfiguration->getField($this->fieldName);
292
293
        if (false === $field->hasValidator($this->validatorName)) {
294
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
295
        }
296
297
        $fieldValidationConfiguration = $field->getValidator($this->validatorName);
298
299
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
300
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
301
        }
302
303
        return $fieldValidationConfiguration;
304
    }
305
306
    /**
307
     * Fetches errors/warnings/notices in the result, and put them in the JSON
308
     * response.
309
     */
310
    protected function injectResultInResponse()
311
    {
312
        $validationName = $this->validation instanceof Validator
313
            ? $this->validation->getName()
314
            : 'default';
315
316
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
317
318
        $result = [
319
            'success'  => !$this->result->hasErrors(),
320
            'data'     => $this->result->getData(),
321
            'messages' => [
322
                'errors'   => $this->formatMessages($validationResult->getErrors()),
323
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
324
                'notices'  => $this->formatMessages($validationResult->getNotices())
325
            ]
326
        ];
327
328
        $this->setUpResponseResult($result);
329
    }
330
331
    /**
332
     * @param array $result
333
     */
334
    protected function setUpResponseResult(array $result)
335
    {
336
        $this->response->setHeader('Content-Type', 'application/json');
337
        $this->response->setContent(json_encode($result));
338
339
        Core::get()->getPageController()->setContentType('application/json');
340
    }
341
342
    /**
343
     * @param FormzMessageInterface[] $messages
344
     * @return array
345
     */
346
    protected function formatMessages(array $messages)
347
    {
348
        $sortedMessages = [];
349
350
        foreach ($messages as $message) {
351
            $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...
352
        }
353
354
        return $sortedMessages;
355
    }
356
357
    /**
358
     * Wrapper for unit tests.
359
     *
360
     * @param RequestInterface  $request
361
     * @param ResponseInterface $response
362
     */
363
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
364
    {
365
        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...
366
    }
367
368
    /**
369
     * Wrapper for unit tests.
370
     */
371
    protected function initializeActionMethodValidatorsParent()
372
    {
373
        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...
374
    }
375
376
    /**
377
     * Used in unit testing.
378
     *
379
     * @param bool $flag
380
     */
381
    public function setProtectedRequestMode($flag)
382
    {
383
        $this->protectedRequestMode = (bool)$flag;
384
    }
385
386
    /**
387
     * @param Exception $exception
388
     * @return string
389
     */
390
    protected function getDebugMessageForException(Exception $exception)
391
    {
392
        return 'Debug mode – ' . $exception->getMessage();
393
    }
394
395
    /**
396
     * @return FormInterface
397
     * @throws MissingArgumentException
398
     */
399
    protected function getForm()
400
    {
401
        return $this->arguments->getArgument($this->formName)->getValue();
402
    }
403
404
    /**
405
     * @return FormObject
406
     */
407
    protected function getFormObject()
408
    {
409
        return FormObjectFactory::get()->registerAndGetFormInstance($this->form, $this->formName);
410
    }
411
412
    /**
413
     * Wrapper for unit tests.
414
     *
415
     * @return Request
416
     */
417
    protected function getRequest()
418
    {
419
        return $this->request;
420
    }
421
}
422