Completed
Branch task/secure-ajax-controller (c67bc7)
by Romain
02:54
created

initializeActionMethodValidators()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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