Completed
Push — wip/steps ( 0e4101...4c32fc )
by Romain
03:57
created

initializeActionMethodValidators()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 6
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\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
            $definition = $this->formObject->getDefinition();
261
262
            if ($definition->hasSteps()) {
263
                $stepIdentifier = $this->formObject->getRequestData()->getCurrentStepIdentifier();
264
265
                if ($definition->getSteps()->hasEntry($stepIdentifier)) {
266
                    $stepService = FormObjectFactory::get()->getStepService($this->formObject);
267
                    $stepService->setCurrentStep($definition->getSteps()->getEntry($stepIdentifier));
268
                }
269
            }
270
271
            $controllerProcessor = ControllerProcessor::prepare($this->request, $this->arguments, $this->getContentObjectSettings());
272
273
            /** @var MiddlewareProcessor $middlewareProcessor */
274
            $middlewareProcessor = Core::instantiate(MiddlewareProcessor::class, $this->formObject, $controllerProcessor);
275
276
            $middlewareProcessor->activateSingleFieldValidationContext();
277
            $middlewareProcessor->run();
278
        } catch (StopPropagationException $exception) {
279
            // @todo exception if forward/redirect?
280
        }
281
    }
282
283
    /**
284
     * @return Validator
285
     * @throws EntryNotFoundException
286
     * @throws InvalidConfigurationException
287
     */
288
    protected function getFieldValidation()
289
    {
290
        $validationResult = $this->formObject->getDefinitionValidationResult();
291
292
        if (true === $validationResult->hasErrors()) {
293
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
294
        }
295
296
        $formConfiguration = $this->formObject->getDefinition();
297
298
        if (false === $formConfiguration->hasField($this->fieldName)) {
299
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
300
        }
301
302
        $field = $formConfiguration->getField($this->fieldName);
303
304
        if (false === $field->hasValidator($this->validatorName)) {
305
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
306
        }
307
308
        $fieldValidationConfiguration = $field->getValidator($this->validatorName);
309
310
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
311
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
312
        }
313
314
        return $fieldValidationConfiguration;
315
    }
316
317
    /**
318
     * Fetches errors/warnings/notices in the result, and put them in the JSON
319
     * response.
320
     */
321
    protected function injectResultInResponse()
322
    {
323
        $validationName = $this->validation instanceof Validator
324
            ? $this->validation->getName()
325
            : 'default';
326
327
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
328
329
        $result = [
330
            'success'  => !$this->result->hasErrors(),
331
            'data'     => $this->result->getData(),
332
            'messages' => [
333
                'errors'   => $this->formatMessages($validationResult->getErrors()),
334
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
335
                'notices'  => $this->formatMessages($validationResult->getNotices())
336
            ]
337
        ];
338
339
        $this->setUpResponseResult($result);
340
    }
341
342
    /**
343
     * @param array $result
344
     */
345
    protected function setUpResponseResult(array $result)
346
    {
347
        $this->response->setHeader('Content-Type', 'application/json');
348
        $this->response->setContent(json_encode($result));
349
350
        Core::get()->getPageController()->setContentType('application/json');
351
    }
352
353
    /**
354
     * @param FormzMessageInterface[] $messages
355
     * @return array
356
     */
357
    protected function formatMessages(array $messages)
358
    {
359
        $sortedMessages = [];
360
361
        foreach ($messages as $message) {
362
            $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...
363
        }
364
365
        return $sortedMessages;
366
    }
367
368
    /**
369
     * Wrapper for unit tests.
370
     *
371
     * @param RequestInterface  $request
372
     * @param ResponseInterface $response
373
     */
374
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
375
    {
376
        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...
377
    }
378
379
    /**
380
     * Wrapper for unit tests.
381
     */
382
    protected function initializeActionMethodValidatorsParent()
383
    {
384
        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...
385
    }
386
387
    /**
388
     * Used in unit testing.
389
     *
390
     * @param bool $flag
391
     */
392
    public function setProtectedRequestMode($flag)
393
    {
394
        $this->protectedRequestMode = (bool)$flag;
395
    }
396
397
    /**
398
     * @param Exception $exception
399
     * @return string
400
     */
401
    protected function getDebugMessageForException(Exception $exception)
402
    {
403
        return 'Debug mode – ' . $exception->getMessage();
404
    }
405
406
    /**
407
     * @return FormInterface
408
     * @throws MissingArgumentException
409
     */
410
    protected function getForm()
411
    {
412
        return $this->arguments->getArgument($this->formName)->getValue();
413
    }
414
415
    /**
416
     * @return FormObject
417
     */
418
    protected function getFormObject()
419
    {
420
        return FormObjectFactory::get()->registerAndGetFormInstance($this->form, $this->formName);
421
    }
422
423
    /**
424
     * Wrapper for unit tests.
425
     *
426
     * @return Request
427
     */
428
    protected function getRequest()
429
    {
430
        return $this->request;
431
    }
432
}
433