Completed
Push — task/secure-ajax-controller ( 60d796 )
by Romain
02:56
created

initializeActionMethodValidatorsParent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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