Completed
Push — middleware ( 3e173d...488069 )
by Romain
02:53
created

AjaxValidationController::getRequestResult()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 1
eloc 16
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 Romm\Formz\Configuration\Form\Field\Validation\Validation;
17
use Romm\Formz\Configuration\Form\Form;
18
use Romm\Formz\Core\Core;
19
use Romm\Formz\Error\FormzMessageInterface;
20
use Romm\Formz\Exceptions\EntryNotFoundException;
21
use Romm\Formz\Exceptions\InvalidConfigurationException;
22
use Romm\Formz\Exceptions\MissingArgumentException;
23
use Romm\Formz\Form\FormInterface;
24
use Romm\Formz\Form\FormObject;
25
use Romm\Formz\Form\FormObjectFactory;
26
use Romm\Formz\Service\ContextService;
27
use Romm\Formz\Service\ExtensionService;
28
use Romm\Formz\Service\MessageService;
29
use Romm\Formz\Validation\DataObject\ValidatorDataObject;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Extbase\Error\Result;
32
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
33
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
34
use TYPO3\CMS\Extbase\Mvc\Web\Request;
35
use TYPO3\CMS\Extbase\Property\PropertyMapper;
36
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
37
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
38
39
class AjaxValidationController extends ActionController
40
{
41
    const ARGUMENT_FORM_CLASS_NAME = 'formClassName';
42
    const ARGUMENT_FORM_NAME = 'formName';
43
    const ARGUMENT_FORM = 'form';
44
    const ARGUMENT_FIELD_NAME = 'fieldName';
45
    const ARGUMENT_VALIDATOR_NAME = 'validatorName';
46
47
    const DEFAULT_ERROR_MESSAGE_KEY = 'default_error_message';
48
49
    /**
50
     * @var array
51
     */
52
    public static $requiredArguments = [
53
        self::ARGUMENT_FORM_CLASS_NAME,
54
        self::ARGUMENT_FORM_NAME,
55
        self::ARGUMENT_FORM,
56
        self::ARGUMENT_FIELD_NAME,
57
        self::ARGUMENT_VALIDATOR_NAME
58
    ];
59
60
    /**
61
     * @var JsonView
62
     */
63
    protected $view;
64
65
    /**
66
     * @var string
67
     */
68
    protected $defaultViewObjectName = JsonView::class;
69
70
    /**
71
     * @var Request
72
     */
73
    protected $request;
74
75
    /**
76
     * @var bool
77
     */
78
    protected $protectedRequestMode = true;
79
80
    /**
81
     * @var string
82
     */
83
    protected $formClassName;
84
85
    /**
86
     * @var string
87
     */
88
    protected $formName;
89
90
    /**
91
     * @var array
92
     */
93
    protected $form;
94
95
    /**
96
     * @var string
97
     */
98
    protected $fieldName;
99
100
    /**
101
     * @var string
102
     */
103
    protected $validatorName;
104
105
    /**
106
     * @var FormObject
107
     */
108
    protected $formObject;
109
110
    /**
111
     * The only accepted method for the request is `POST`.
112
     */
113
    public function initializeAction()
114
    {
115
        if ($this->request->getMethod() !== 'POST') {
116
            $this->throwStatus(400);
117
        }
118
    }
119
120
    public function getView()
121
    {
122
        return $this->view;
123
    }
124
125
    /**
126
     * Main action that will render the validation result.
127
     */
128
    public function runAction()
129
    {
130
        $result = ($this->protectedRequestMode)
131
            ? $this->getProtectedRequestResult()
132
            : $this->getRequestResult();
133
134
        $this->view->setVariablesToRender(['result']);
135
        $this->view->assign('result', $result);
136
    }
137
138
    /**
139
     * @param bool $flag
140
     */
141
    public function setProtectedRequestMode($flag)
142
    {
143
        $this->protectedRequestMode = (bool)$flag;
144
    }
145
146
    /**
147
     * Will fetch the result and prevent any exception or external message to be
148
     * displayed.
149
     *
150
     * @return array
151
     */
152
    protected function getProtectedRequestResult()
153
    {
154
        // Default technical error result if the function can not be reached.
155
        $result = [
156
            'success' => false,
157
            'messages' => [
158
                'errors' => ['default' => ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY)]
159
            ]
160
        ];
161
162
        // We prevent any external message to be displayed here.
163
        ob_start();
164
165
        try {
166
            $result = $this->getRequestResult();
167
        } catch (\Exception $exception) {
168
            $result['data'] = ['errorCode' => $exception->getCode()];
169
170
            if (ExtensionService::get()->isInDebugMode()) {
171
                $result['messages']['errors']['default'] = $this->getDebugMessageForException($exception);
172
            }
173
        }
174
175
        ob_end_clean();
176
177
        return $result;
178
    }
179
180
    /**
181
     * Will get the result of the validation for this Ajax request.
182
     *
183
     * If any error is found, an exception is thrown.
184
     *
185
     * @return array
186
     */
187
    protected function getRequestResult()
188
    {
189
        $this->initializeArguments();
190
191
        $this->formObject = $this->getFormObject();
192
        $this->checkConfigurationValidationResult();
193
        $validation = $this->getFieldValidation();
194
        $form = $this->buildFormObject();
195
        $this->formObject->setForm($form);
196
        $fieldValue = ObjectAccess::getProperty($form, $this->fieldName);
197
        $validatorDataObject = new ValidatorDataObject($this->formObject, $validation);
198
199
        /** @var ValidatorInterface $validator */
200
        $validator = GeneralUtility::makeInstance(
201
            $validation->getClassName(),
202
            $validation->getOptions(),
203
            $validatorDataObject
204
        );
205
206
        $result = $validator->validate($fieldValue);
207
        $result = MessageService::get()->sanitizeValidatorResult($result, $validation);
208
209
        return $this->convertResultToJson($result);
210
    }
211
212
    /**
213
     * Initializes all arguments for the request, and returns an array
214
     * containing the missing arguments.
215
     */
216
    protected function initializeArguments()
217
    {
218
        $argumentsMissing = [];
219
220
        foreach (self::$requiredArguments as $argument) {
221
            $argumentValue = $this->getArgument($argument);
222
223
            if ($argumentValue) {
224
                $this->$argument = $argumentValue;
225
            } else {
226
                $argumentsMissing[] = $argument;
227
            }
228
        }
229
230
        if (false === empty($argumentsMissing)) {
231
            throw MissingArgumentException::ajaxControllerMissingArguments($argumentsMissing);
232
        }
233
    }
234
235
    /**
236
     * @return FormObject
237
     */
238
    protected function getFormObject()
239
    {
240
        /** @var FormObjectFactory $formObjectFactory */
241
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
242
243
        return $formObjectFactory->getInstanceFromClassName($this->formClassName, $this->formName);
244
    }
245
246
    /**
247
     * @throws InvalidConfigurationException
248
     */
249
    protected function checkConfigurationValidationResult()
250
    {
251
        $validationResult = $this->formObject->getConfigurationValidationResult();
252
253
        if (true === $validationResult->hasErrors()) {
254
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
255
        }
256
    }
257
258
    /**
259
     * @return Validation
260
     * @throws EntryNotFoundException
261
     * @throws InvalidConfigurationException
262
     */
263
    protected function getFieldValidation()
264
    {
265
        $formConfiguration = $this->getFormConfiguration($this->formObject);
266
        $field = $formConfiguration->getField($this->fieldName);
267
268
        if (false === $field->hasValidation($this->validatorName)) {
269
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
270
        }
271
272
        $fieldValidationConfiguration = $field->getValidationByName($this->validatorName);
273
274
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
275
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
276
        }
277
278
        return $fieldValidationConfiguration;
279
    }
280
281
    /**
282
     * @param FormObject $formObject
283
     * @return Form
284
     * @throws EntryNotFoundException
285
     */
286
    protected function getFormConfiguration(FormObject $formObject)
287
    {
288
        $formConfiguration = $formObject->getConfiguration();
289
290
        if (false === $formConfiguration->hasField($this->fieldName)) {
291
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $formObject);
292
        }
293
294
        return $formConfiguration;
295
    }
296
297
    /**
298
     * Will build and fill an object with a form sent value.
299
     *
300
     * @return FormInterface
301
     */
302
    protected function buildFormObject()
303
    {
304
        $values = $this->cleanValuesFromUrl($this->form);
305
306
        return $this->getPropertyMapper()->convert($values, $this->formClassName);
307
    }
308
309
    /**
310
     * Will convert the result of the function called by this class in a JSON
311
     * string.
312
     *
313
     * @param Result $result
314
     * @return array
315
     */
316
    protected function convertResultToJson(Result $result)
317
    {
318
        $messages = [];
319
320
        if ($result->hasErrors()) {
321
            $messages['errors'] = $this->formatMessages($result->getErrors());
0 ignored issues
show
Documentation introduced by
$result->getErrors() is of type array<integer,object<TYP...S\Extbase\Error\Error>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
322
        }
323
324
        if ($result->hasWarnings()) {
325
            $messages['warnings'] = $this->formatMessages($result->getWarnings());
0 ignored issues
show
Documentation introduced by
$result->getWarnings() is of type array<integer,object<TYP...Extbase\Error\Warning>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
326
        }
327
328
        if ($result->hasNotices()) {
329
            $messages['notices'] = $this->formatMessages($result->getNotices());
0 ignored issues
show
Documentation introduced by
$result->getNotices() is of type array<integer,object<TYP...\Extbase\Error\Notice>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
330
        }
331
332
        return [
333
            'success' => !$result->hasErrors(),
334
            'messages' => $messages
335
        ];
336
    }
337
338
    /**
339
     * @param FormzMessageInterface[] $messages
340
     * @return array
341
     */
342
    protected function formatMessages(array $messages)
343
    {
344
        $sortedMessages = [];
345
346
        foreach ($messages as $message) {
347
            $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...
348
        }
349
350
        return $sortedMessages;
351
    }
352
353
    /**
354
     * @param \Exception $exception
355
     * @return string
356
     */
357
    protected function getDebugMessageForException(\Exception $exception)
358
    {
359
        return 'Debug mode – ' . $exception->getMessage();
360
    }
361
362
    /**
363
     * @param string $name
364
     * @return mixed
365
     */
366
    protected function getArgument($name)
367
    {
368
        return GeneralUtility::_GP($name);
369
    }
370
371
    /**
372
     * Will clean the string filled with form values sent with Ajax.
373
     *
374
     * @param array $values
375
     * @return array
376
     */
377
    protected function cleanValuesFromUrl($values)
378
    {
379
        // Cleaning the given form values.
380
        $values = reset($values);
381
        unset($values['__referrer']);
382
        unset($values['__trustedProperties']);
383
384
        return reset($values);
385
    }
386
387
    /**
388
     * @return PropertyMapper
389
     */
390
    protected function getPropertyMapper()
391
    {
392
        /** @var PropertyMapper $propertyMapper */
393
        $propertyMapper = Core::instantiate(PropertyMapper::class);
394
395
        return $propertyMapper;
396
    }
397
}
398